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 con T&&
). La razón por la que esto funciona es porque una "referencia universal", auto&&
o T&&
, 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 una const
referencia es que es const
! Posteriormente, no podrá vincularlo a ninguna referencia que no sea constante o invocar funciones miembro que no estén marcadas const
.
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:
auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;
Este código se compilará bien independientemente de la expresión inicializadora. Las alternativas a auto&&
fallar de las siguientes maneras:
auto => will copy the vector, but we wanted a reference
auto& => will only bind to modifiable lvalues
const auto& => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues
Entonces, para esto, ¡ auto&&
funciona perfectamente! Un ejemplo de uso auto&&
como este es en un for
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:
auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));
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 que auto&&
se unirá a cualquier cosa, no podemos tratar de extraervar
tripas nosotros mismos, puede muy bien ser un valor o incluso constante. Sin embargo, podemos std::forward
llevarlo a otras funciones que pueden devastar totalmente sus entrañas. Tan pronto como hagamos esto, deberíamos considerar var
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 un T
by por valor. En este caso, sabemos con certeza que el tipo de var
se deducirá como T&&
. Como sabemos con certeza que es un valor, no necesitamos std::forward
el permiso para robar sus recursos. En este caso específico, sabiendo que foo
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:
std::vector<int> global_vec{1, 2, 3, 4};
template <typename T>
T get_vector()
{
return global_vec;
}
template <typename T>
void foo()
{
auto&& vec = get_vector<T>();
auto i = std::begin(vec);
(*i)++;
std::cout << vec[0] << std::endl;
}
Aquí get_vector<T>()
está esa hermosa expresión que podría ser un valor o un valor según el tipo genérico T
. Esencialmente cambiamos el tipo de retorno deget_vector
través del parámetro de plantilla de foo
.
Cuando llamamos foo<std::vector<int>>
, get_vector
regresará global_vec
por valor, lo que da una expresión de valor. Alternativamente, cuando llamemos foo<std::vector<int>&>
, get_vector
regresaremosglobal_vec
por referencia, lo que resultado una expresión de valor.
Si lo hacemos:
foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;
Obtenemos el siguiente resultado, como se esperaba:
2
1
2
2
Si se va a cambiar el auto&&
en el código a cualquiera de auto
, auto&
, const auto&
, o const 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:
if (std::is_lvalue_reference<decltype(var)>::value) {
// var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
// var was initialised with an rvalue expression
}
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.