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 constreferencia 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 forbucle basado en rango . Vea mi otra pregunta para más detalles.
Si luego lo usa std::forwarden 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_elsewhereextraer 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::forwardllevarlo a otras funciones que pueden devastar totalmente sus entrañas. Tan pronto como hagamos esto, deberíamos considerar varestar 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 Tby por valor. En este caso, sabemos con certeza que el tipo de varse deducirá como T&&. Como sabemos con certeza que es un valor, no necesitamos std::forwardel permiso para robar sus recursos. En este caso específico, sabiendo que fooretorna 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_lvaluepodrí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_vectorregresará global_vecpor valor, lo que da una expresión de valor. Alternativamente, cuando llamemos foo<std::vector<int>&>, get_vectorregresaremosglobal_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.