Ao usar o que auto&& var = <initializer>
você está dizendo: Aceitarei qualquer inicializador, independentemente de ser uma expressão lvalue ou rvalue, e preservarei sua constância . Isso geralmente é usado para encaminhamento (geralmente com T&&
). A razão pela qual isso funciona é porque uma "referência universal", auto&&
ou T&&
, se ligará a qualquer coisa .
Você pode dizer, bem, por que não usar um const auto&
porque isso também se vincula a alguma coisa? O problema de usar uma const
referência é que é const
! Mais tarde, você não poderá vinculá-lo a nenhuma referência que não seja const nem chamar funções de membros que não estejam marcadas const
.
Como exemplo, imagine que você deseja obter a std::vector
, leve um iterador ao seu primeiro elemento e modifique o valor apontado por esse iterador de alguma forma:
auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;
Esse código será compilado corretamente, independentemente da expressão do inicializador. As alternativas para auto&&
falhar das seguintes maneiras:
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
Então, para isso, auto&&
funciona perfeitamente! Um exemplo de uso auto&&
como este está em um for
loop baseado em intervalo . Veja minha outra pergunta para mais detalhes.
Se você usar std::forward
sua auto&&
referência para preservar o fato de que era originalmente um lvalue ou um rvalue, seu código diz: Agora que obtive seu objeto de uma expressão lvalue ou rvalue, desejo preservar a valor que ele originalmente para que eu possa usá-lo com mais eficiência - isso pode invalidá-lo. Como em:
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));
Isso permite use_it_elsewhere
extrair suas tripas por uma questão de desempenho (evitando cópias) quando o inicializador original tinha um valor modificável.
O que isso significa se podemos ou quando podemos roubar recursos var
? Bem, como a auto&&
vontade se liga a qualquer coisa, não podemos tentar arrancar a var
coragem de nós mesmos - pode muito bem ser um valor ou até uma const. No entanto, podemos std::forward
fazê-lo para outras funções que podem devastar totalmente seu interior. Assim que fizermos isso, devemos considerar var
um estado inválido.
Agora vamos aplicar isso ao caso de auto&& var = foo();
, como indicado na sua pergunta, em que foo retorna a T
por valor. Nesse caso, sabemos com certeza que o tipo de var
será deduzido como T&&
. Como sabemos com certeza que é um valor, não precisamos std::forward
da permissão para roubar seus recursos. Nesse caso específico, sabendo que foo
retorna por valor , o leitor deve apenas lê-lo da seguinte maneira: Estou fazendo uma referência de valor-valor ao temporário retornado de foo
, para que eu possa mover-me com prazer.
Como um adendo, acho que vale a pena mencionar quando uma expressão como some_expression_that_may_be_rvalue_or_lvalue
pode aparecer, além de uma situação "bem, seu código pode mudar". Então, aqui está um exemplo 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;
}
Aqui get_vector<T>()
está essa expressão adorável que pode ser um lvalue ou rvalue, dependendo do tipo genérico T
. Nós basicamente alteramos o tipo de retorno get_vector
através do parâmetro template de foo
.
Quando chamamos foo<std::vector<int>>
, get_vector
retornará global_vec
por valor, o que fornece uma expressão rvalue. Como alternativa, quando chamamos foo<std::vector<int>&>
, get_vector
retornará global_vec
por referência, resultando em uma expressão lvalue.
Se nós fizermos:
foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;
Obtemos a seguinte saída, conforme o esperado:
2
1
2
2
Se você fosse para mudar o auto&&
no código para qualquer um auto
, auto&
, const auto&
, ou const auto&&
então nós não vai conseguir o resultado que queremos.
Uma maneira alternativa de alterar a lógica do programa com base no fato de sua auto&&
referência ser inicializada com uma expressão lvalue ou rvalue é usar características 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&&
? Eu estive pensando em analisar por que um loop for baseado em intervalo se expande para usarauto&&
como exemplo, mas ainda não chegou a esse ponto. Talvez quem quer que responda possa explicar.