c++ c++11 reference universal rvalue

c++ - ¿Por qué las "referencias universales" tienen la misma sintaxis que las referencias rvalue?



c++11 reference (5)

Creo que sucedió al revés. La idea inicial era introducir referencias rvalue en el lenguaje, lo que significa que "al código que proporciona la referencia de doble ampersand no le importa lo que sucederá con el objeto referido". Esto permite mover la semántica. Esto es bonito.

Ahora. La norma prohíbe construir una referencia a una referencia, pero esto siempre fue posible. Considerar:

template<typename T> void my_func(T, T&) { /* ... */ } // ... my_func<int&>(a, b);

En este caso, el tipo del segundo parámetro debe ser int & & , pero esto está explícitamente prohibido en el estándar. Entonces, las referencias deben colapsarse, incluso en C ++ 98. En C ++ 98, solo había un tipo de referencia, por lo que la regla de colapso era simple:

& & -> &

Ahora, tenemos dos tipos de referencias, donde && significa "No me importa lo que pueda pasarle al objeto", y significa "Me puede importar lo que pueda sucederle al objeto, así que mejor mire lo que está obra". Con esto en mente, las reglas colapsadas fluyen naturalmente: C ++ debe colapsar las referencias a && solo si a nadie le importa lo que le sucede al objeto:

& & -> & & && -> & && & -> & && && -> &&

Con estas reglas en su lugar, creo que es Scott Meyers quien notó que este subconjunto de reglas:

& && -> & && && -> &&

Muestra que && es correcto-neutral con respecto al colapso de referencia, y, cuando se produce una deducción de tipo, el constructo T&& puede usarse para coincidir con cualquier tipo de referencia, y acuñó el término "Referencia universal" para estas referencias. No es algo inventado por el Comité. Es solo un efecto secundario de otras reglas, no un diseño del Comité.

Y, por lo tanto, se ha introducido el término para distinguir entre referencias de valores R reales, cuando no se produce ninguna deducción, que se garantiza que son && , y esas referencias UNIVERSAL deducidas de tipo, que no se garantiza que permanezcan && en el tiempo de especialización de plantilla.

Acabo de hacer algunas investigaciones sobre esas (bastante) nuevas funciones y me pregunto por qué el Comité C ++ decidió introducir la misma sintaxis para ambos. Parece que los desarrolladores deben perder tiempo para entender cómo funciona, y una solución permite pensar en más problemas. En mi caso, comenzó desde un problema que se puede simplificar a esto:

#include <iostream> template <typename T> void f(T& a) { std::cout << "f(T& a) for lvalues/n"; } template <typename T> void f(T&& a) { std::cout << "f(T&& a) for rvalues/n"; } int main() { int a; f(a); f(int()); return 0; }

Lo compilé primero en VS2013 y funcionó como esperaba, con estos resultados:

f(T& a) for lvalues f(T&& a) for rvalues

Pero había una cosa sospechosa: intellisense subrayado f (a). Hice algunas investigaciones y entendí que es porque el tipo colapsa (referencias universales como lo llamó Scott Meyers), así que me pregunté qué piensa G ++ al respecto. Por supuesto que no compiló. Es muy bueno que Microsoft haya implementado su compilador para que funcione de una forma más intuitiva, pero no estoy seguro si es de acuerdo con el estándar y si debería haber este tipo de diferencia en IDE (compilador vs intellisense, pero de hecho puede haber tener algo de sentido en esto). Ok, vuelve al problema. Lo resolví de esta manera:

template <typename T> void f(T& a) { std::cout << "f(T& a) for lvalues/n"; } template <typename T> void f(const T&& a) { std::cout << "f(T&& a) for rvalues/n"; }

Ahora no había ningún tipo de colapso, solo sobrecarga normal para valores (r / l). Se compiló en g ++, Intellisense dejó de quejarse y yo estaba casi satisfecho. Casi, porque pensé qué pasaría si quisiera cambiar algo en el estado del objeto que pasa por referencia de valor real. Podría describir alguna situación cuando podría ser necesario, pero esta descripción es demasiado larga para presentarla aquí. Lo resolví de esta manera:

template <typename T> void f(T&& a, std::true_type) { std::cout << "f(T&& a) for rvalues/n"; } template <typename T> void f(T&& a, std::false_type) { std::cout << "f(T&& a) for lvalues/n"; } template <typename T> void f(T&& a) { f(std::forward<T>(a), std::is_rvalue_reference<T&&>()); }

Ahora se compila en todos los compiladores probados y me permite cambiar el estado del objeto en la implementación de referencia rvalue, pero no se ve muy bien, y esto se debe a la misma sintaxis para referencias universales y referencias rvalue. Entonces mi pregunta es: ¿Por qué el Comité C ++ no introdujo alguna otra sintaxis para las referencias universales? Creo que esta característica debe estar señalizada, por ejemplo, por T ?, auto ?, o algo similar, pero no como T && y auto && que solo colisionan con las referencias de valor. Usando este enfoque, mi primera implementación sería perfectamente correcta, no solo para el compilador de MS. ¿Alguien puede explicar la decisión del Comité?


Las referencias universales funcionan debido a las reglas de colapso de referencia en c ++ 11. si usted tiene

template <typename T> func (T & t)

El colapso de referencia todavía ocurre pero no funcionará con un temporal, por lo que la referencia no es "universal". Una referencia universal se llama "universal" porque puede aceptar lvals y rvals (también conserva otros calificadores). T & t no es universal, ya que no puede aceptar rvals.

Para resumir, las referencias universales son un producto de colapso de referencia, y una referencia universal se denomina así porque es universal, es lo que puede ser


Otros ya han mencionado que las reglas de colapso de referencia son clave para las referencias universales al trabajo, pero hay otro aspecto (igualmente) igualmente importante: deducción del argumento de la plantilla cuando el parámetro de la plantilla es de la forma T&& .

En realidad, en relación con la pregunta:

¿Por qué las "referencias universales" tienen la misma sintaxis que las referencias rvalue?

en mi opinión, la forma del parámetro de plantilla es más importante porque todo se trata de sintaxis. En C ++ 03 no había forma de que una función de plantilla conociera la categoría de valor (rvalue o lvalue) del objeto pasado. C ++ 11 cambió la deducción del argumento de la plantilla para dar cuenta de eso: 14.8.2.1 [temp.deduct.call] / p3

[...] Si P es una referencia rvalue a un parámetro de plantilla cv no calificada y el argumento es un valor l, se usa el tipo "referencia de lvalue A " en lugar de A para la deducción de tipo.

Esto es un poco más complicado que la redacción originalmente propuesta (dada por n1770 ):

Si P es un tipo de referencia rvalue de la forma cv T&& donde T es un parámetro de tipo de plantilla, y el argumento es un valor T , el valor del argumento de la plantilla deducida para T es A& . [Ejemplo:

template<typename T> int f(T&&); int i; int j = f(i); // calls f<int&>(i)

--- fin del ejemplo]

Con más detalle, la llamada anterior activa la instanciación de f<int&>(int& &&) que, después de que se aplique el colapso de referencia, se convierte en f<int&>(int&) . Por otro lado f(0) crea instancias f<int>(int&&) . (Tenga en cuenta que no hay & dentro < ... > )

Ninguna otra forma de declaración deduciría T a int& y desencadenaría la instanciación de f<int&>( ... ) . (Tenga en cuenta que a & puede aparecer entre ( ... ) pero no entre < ... > .)

En resumen, cuando se realiza una deducción de tipo, la forma sintáctica T&& es lo que permite que la categoría de valor del objeto original esté disponible dentro del cuerpo de la plantilla de función.

Relacionado con este hecho, observe que uno debe usar std::forward<T>(arg) y no std::forward(arg) exactamente porque es T (no arg ) que registra la categoría de valor del objeto original. (Como medida de precaución, la definición de std::forward "artificialmente" obliga a la última compilación a no evitar que los programadores cometan este error).

Volver a la pregunta original: "¿Por qué el comité decidió usar el formulario T&& lugar de elegir una nueva sintaxis?"

No puedo decir la verdadera razón, pero puedo especular. Primero, esto es retrocompatible con C ++ 03. En segundo lugar, y lo más importante, esta era una solución muy simple para indicar en el Estándar (cambio de un párrafo) y para implementarla por los compiladores. Por favor, no me malinterpreten. No digo que los miembros del comité sean flojos (ciertamente no lo son). Solo digo que minimizaron el riesgo de daños colaterales.


Respondió a su propia pregunta: "referencia universal" es solo un nombre para el caso de referencia rvalue de colapso de referencia. Si se necesitara otra sintaxis para el colapso de referencia, ya no sería una referencia de colapso. El colapso de referencia es simplemente aplicar un calificador de referencia a un tipo de referencia.

así que me pregunté qué piensa G ++ al respecto. Por supuesto que no compiló.

Tu primer ejemplo está bien formado. GCC 4.9 lo compila sin quejarse, y el resultado coincide con MSVC.

Casi, porque pensé en qué pasaría si quisiera cambiar algo en el estado del objeto que pasa por referencia de valor real.

Las referencias de Rvalue no aplican la semántica de const ; siempre puede cambiar el estado de un objeto pasado por move . La mutabilidad es necesaria para su propósito. Aunque existe algo como const && , nunca deberías necesitarlo.


En primer lugar , la razón por la cual el primer ejemplo no se compiló con gcc 4.8 es que este es un error en gcc 4.8. (Lo ampliaré más adelante). El primer ejemplo compila, ejecuta y produce la misma salida que VS2013 con las versiones posteriores a 4.8 de gcc, clang 3.3 y posterior, y el compilador de c ++ basado en LLVM de Apple.

En referencias universales:
Una razón por la cual Scott Meyer acuñó el término "referencias universales" se debe a que T&& como un argumento de plantilla de función coincide tanto con los valores l como con los valores r. La universalidad de T&& en las plantillas se puede ver eliminando la primera función del primer ejemplo en la pregunta:

// Example 1, sans f(T&): #include <iostream> #include <type_traits> template <typename T> void f(T&&) { std::cout << "f(T&&) for universal references/n"; std::cout << "T&& is an rvalue reference: " << std::boolalpha << std::is_rvalue_reference<T&&>::value << ''/n''; } int main() { int a; const int b = 42; f(a); f(b); f(0); }

Lo anterior compila y ejecuta en todos los compiladores antes mencionados, y también en gcc 4.8. Esta única función acepta universalmente lvalues ​​y rvalues ​​como argumentos. En el caso de las llamadas a f(a) f(b) , la función informa que T&& no es una referencia de valor r. Las llamadas a f(a) , f(b) f(0) convierten respectivamente en llamadas a las funciones f<int&>(int&) , f<const int&>(const int&) , f<int&&>(int&&) . Solo en el caso de f(0) es el T&& convertido en una referencia de valor. Dado que un argumento T&& foo puede ser o no una referencia de valor en el caso de una plantilla de función, lo mejor es llamar a esas otras cosas. Meyers eligió llamarlos "referencias universales".

Por qué esto es un error en gcc 4.8:
En el primer código de ejemplo de la pregunta, la plantilla de template <typename T> void f(T&) función template <typename T> void f(T&) y template <typename T> void f(T&&) convierten en f<int>(int&) y f<int&>(int&) con respecto a la llamada a f(a) , esta última gracias a las reglas de colapso de referencia de C ++ 11. Estas dos funciones tienen exactamente la misma firma, por lo que tal vez gcc 4.8 es correcto, la llamada a f(a) es ambigua. No lo es.

La razón por la que la llamada no es ambigua es que la template <typename T> void f(T&) es más especializada que la template <typename T> void f(T&&) según las reglas de las secciones 13.3 (Resolución de sobrecarga), 14.5.6.2 (Parcial ordenamiento de plantillas de función), y 14.8.2.4 (Deducir argumentos de plantilla durante el pedido parcial). Al comparar la template <typename T> void f(T&&) a la template <typename T> void f(T&) con T=int& , ambos productos f(int&) . No se puede hacer distinción aquí. Sin embargo, al comparar la template<typename T> void f(T&) a la template <typename T> void f(T&&) con T=int , la primera es más especializada porque ahora tenemos f(int&) contra f(int&&) . Según 14.8.2.4 párrafo 9, "si el tipo de la plantilla del argumento era una referencia lvalue y el tipo de la plantilla del parámetro no, el tipo de argumento se considera más especializado que el otro".