Si lees código como
auto&& var = foo();
donde foo
es cualquier función que regresa por valor de tipo T
. Entonces var
hay un lvalue de tipo rvalue referencia a T
. ¿Pero para qué implica esto var
? ¿Significa que se nos permite robar los recursos de var
? ¿Hay situaciones razonables en las que debería usar auto&&
para decirle al lector de su código algo como lo hace cuando devuelve un mensaje unique_ptr<>
para decirle que tiene la propiedad exclusiva? ¿Y qué pasa, por ejemplo, T&&
cuando T
es de tipo de clase?
Solo quiero entender, si hay otros casos de uso auto&&
distintos a los de la programación de plantillas; como los discutidos en los ejemplos de este artículo Referencias universales de Scott Meyers.
auto&&
? He estado pensando en por qué un bucle basado en rango se expande para usarloauto&&
como ejemplo, pero no he llegado a eso. Quizás quien responda pueda explicarlo.foo
retornos, almacenar un valor de referencia a él suena como UB a ne.foo
la fuerza, por ejemplo, el siguiente aspecto:int foo(){return 1;}
.Respuestas:
Al usarlo
auto&& var = <initializer>
estás diciendo: aceptaré cualquier inicializador independientemente de si es una expresión de valor o valor y preservaré su constidad . Esto se usa generalmente para reenviar (generalmente conT&&
). La razón por la que esto funciona es porque una "referencia universal",auto&&
oT&&
, se unirá a cualquier cosa .Podrías decir, bueno, ¿por qué no solo usar un
const auto&
porque eso también se unirá a cualquier cosa? El problema con el uso de unaconst
referencia es que esconst
! Posteriormente, no podrá vincularlo a ninguna referencia que no sea constante o invocar funciones miembro que no estén marcadasconst
.Como ejemplo, imagine que desea obtener un
std::vector
, tome un iterador a su primer elemento y modifique el valor señalado por ese iterador de alguna manera:Este código se compilará bien independientemente de la expresión inicializadora. Las alternativas a
auto&&
fallar de las siguientes maneras:Entonces, para esto, ¡
auto&&
funciona perfectamente! Un ejemplo de usoauto&&
como este es en unfor
bucle basado en rango . Vea mi otra pregunta para más detalles.Si luego lo usa
std::forward
en suauto&&
referencia para preservar el hecho de que originalmente era un valor l o un valor r, su código dice: Ahora que obtuve su objeto de una expresión lvalue o rvalue, quiero preservar cualquier valor originalmente tenía para poder usarlo de manera más eficiente, esto podría invalidarlo. Como en:Esto permite
use_it_elsewhere
extraer sus agallas por el bien del rendimiento (evitando copias) cuando el inicializador original era un valor variable modificable.¿Qué significa esto en cuanto a si podemos o cuándo podemos robar recursos
var
? Bueno, ya queauto&&
se unirá a cualquier cosa, no podemos tratar de extraervar
tripas nosotros mismos, puede muy bien ser un valor o incluso constante. Sin embargo, podemosstd::forward
llevarlo a otras funciones que pueden devastar totalmente sus entrañas. Tan pronto como hagamos esto, deberíamos considerarvar
estar en un estado no válido.Ahora apliquemos esto al caso de
auto&& var = foo();
, como se indica en su pregunta, donde foo devuelve unT
by por valor. En este caso, sabemos con certeza que el tipo devar
se deducirá comoT&&
. Como sabemos con certeza que es un valor, no necesitamosstd::forward
el permiso para robar sus recursos. En este caso específico, sabiendo quefoo
retorna por valor , el lector debería leerlo como: Estoy tomando una referencia de valor al temporal devuelto porfoo
, por lo que felizmente puedo pasar de él.Como un apéndice, creo que vale la pena mencionar cuándo
some_expression_that_may_be_rvalue_or_lvalue
podría aparecer una expresión como , además de una situación de "bien, su código podría cambiar". Así que aquí hay un ejemplo artificial:Aquí
get_vector<T>()
está esa hermosa expresión que podría ser un valor o un valor según el tipo genéricoT
. Esencialmente cambiamos el tipo de retorno deget_vector
través del parámetro de plantilla defoo
.Cuando llamamos
foo<std::vector<int>>
,get_vector
regresaráglobal_vec
por valor, lo que da una expresión de valor. Alternativamente, cuando llamemosfoo<std::vector<int>&>
,get_vector
regresaremosglobal_vec
por referencia, lo que resultado una expresión de valor.Si lo hacemos:
Obtenemos el siguiente resultado, como se esperaba:
Si se va a cambiar el
auto&&
en el código a cualquiera deauto
,auto&
,const auto&
, oconst auto&&
entonces no conseguimos el resultado que queremos.Una forma alternativa de cambiar la lógica del programa en función de si su
auto&&
referencia se inicializa con una expresión lvalue o rvalue es usar rasgos de tipo:fuente
T vec = get_vector<T>();
dentro de la función foo? ¿O lo estoy simplificando a un nivel absurdo :)Primero, recomiendo leer esta respuesta mía como una lectura paralela para una explicación paso a paso sobre cómo funciona la deducción de argumentos de plantilla para referencias universales.
No necesariamente. ¿Qué sucede si
foo()
de repente devuelve una referencia o si cambió la llamada pero olvidó actualizar el uso devar
? O si está en código genérico y el tipo de retorno defoo()
puede cambiar según sus parámetros?Piense
auto&&
que ser exactamente la misma que laT&&
detemplate<class T> void f(T&& v);
, porque es (casi † ) exactamente eso. ¿Qué haces con las referencias universales en las funciones, cuando necesitas pasarlas o usarlas de alguna manera? Se utilizastd::forward<T>(v)
para recuperar la categoría de valor original. Si era un valor de l antes de pasar a su función, permanece como un valor de l después de pasarlostd::forward
. Si fue un valor r, se convertirá en un valor r nuevamente (recuerde, una referencia de valor r nombrada es un valor l).Entonces, ¿cómo se usa
var
correctamente de manera genérica? Usostd::forward<decltype(var)>(var)
. Esto funcionará exactamente igual questd::forward<T>(v)
en la plantilla de función anterior. Sivar
es unT&&
, obtendrá un valor de retorno, y si es asíT&
, obtendrá un valor de retorno.Entonces, volviendo al tema: ¿Qué nos dicen
auto&& v = f();
ystd::forward<decltype(v)>(v)
en una base de código? Nos dicen quev
será adquirido y transmitido de la manera más eficiente. Sin embargo, recuerde que después de haber reenviado una variable de este tipo, es posible que se haya movido de ella, por lo que sería incorrecto usarla aún más sin reiniciarla.Personalmente, lo uso
auto&&
en código genérico cuando necesito una variable modificable . El reenvío perfecto de un valor r está modificando, ya que la operación de movimiento potencialmente le roba sus entrañas. Si solo quiero ser vago (es decir, no deletrear el nombre del tipo, incluso si lo sé) y no necesito modificar (por ejemplo, cuando solo imprimo elementos de un rango), me atendréauto const&
.†
auto
es diferente en la medida en que seauto v = {1,2,3};
va a hacerv
unastd::initializer_list
, mientras quef({1,2,3})
será un fracaso deducción.fuente
foo()
devuelve un tipo de valorT
, entoncesvar
(esta expresión) será un valor l y su tipo (de esta expresión) será una referencia de valor rT
(es decirT&&
).Considere algún tipo
T
que tenga un constructor de movimiento, y supongausa ese constructor de movimiento.
Ahora, usemos una referencia intermedia para capturar el retorno de
foo
:esto descarta el uso del constructor de movimiento, por lo que el valor de retorno tendrá que copiarse en lugar de moverse (incluso si lo usamos
std::move
aquí, en realidad no podemos movernos a través de una referencia constante)Sin embargo, si usamos
el constructor de movimiento todavía está disponible.
Y para abordar sus otras preguntas:
Lo primero, como dice Xeo, es esencialmente que estoy pasando X de la manera más eficiente posible , sea cual sea el tipo X. Entonces, ver el código que usa
auto&&
internamente debería comunicar que usará la semántica de movimiento internamente cuando sea apropiado.Cuando una plantilla de función toma un argumento de tipo
T&&
, dice que puede mover el objeto que pasa. Al regresarunique_ptr
explícitamente se le otorga la propiedad al llamante; aceptarT&&
puede eliminar la propiedad de la persona que llama (si existe un movimiento, etc.).fuente
ref
yrvref
son ambos lvalues. Si quieres el constructor de movimiento, entonces tienes que escribirT t(std::move(rvref))
.auto const &
:?auto&&
y qué le dices al lector de tu código usandoauto&&
?La
auto &&
sintaxis utiliza dos nuevas características de C ++ 11:La
auto
parte permite que el compilador deduzca el tipo según el contexto (el valor de retorno en este caso). Esto es sin ninguna calificación de referencia (lo que le permite especificar si lo deseaT
,T &
oT &&
para un tipo deducidoT
).El
&&
es el nuevo movimiento semántico. Un tipo que admite semántica de movimiento implementa un constructorT(T && other)
que mueve de manera óptima el contenido en el nuevo tipo. Esto permite que un objeto intercambie la representación interna en lugar de realizar una copia profunda.Esto le permite tener algo como:
Entonces:
realizará una copia del vector devuelto (caro), pero:
intercambiará la representación interna del vector (el vector de
foo
y el vector vacío devar
), por lo que será más rápido.Esto se usa en la nueva sintaxis for-loop:
Donde el ciclo for está reteniendo un
auto &&
valor de retornofoo
yitem
es una referencia a cada valor enfoo
.fuente
auto&&
no moverá nada, solo hará una referencia. Si se trata de una referencia lvalue o rvalue depende de la expresión utilizada para inicializarla.std::vector
ystd::string
son moveconstructible. Esto no tiene nada que ver con el tipo devar
.