O que o auto && nos diz?

168

Se você ler código como

auto&& var = foo();

onde fooé qualquer função que retorna pelo valor do tipo T. Então varé um lvalue do tipo rvalue de referência T. Mas o que isso implica var? Isso significa que estamos autorizados a roubar os recursos var? Existem situações razoáveis ​​em que você deve auto&&informar ao leitor do seu código algo parecido com o que você faz quando retorna a unique_ptr<>para informar que possui propriedade exclusiva? E o que dizer, por exemplo, T&&quando Té do tipo classe?

Eu só quero entender, se existem outros casos de uso auto&&além daqueles na programação de modelos; como os discutidos nos exemplos deste artigo, Referências universais de Scott Meyers.

MWid
fonte
1
Eu estive pensando a mesma coisa. Entendo como funciona a dedução de tipo, mas o que meu código está dizendo quando uso auto&&? Eu estive pensando em analisar por que um loop for baseado em intervalo se expande para usar auto&&como exemplo, mas ainda não chegou a esse ponto. Talvez quem quer que responda possa explicar.
Joseph Mansfield
1
Isso é legal? Quero dizer, o instinto de T é destruído imediatamente após os fooretornos, armazenando um valor de referência a ele como UB a ne.
1
@aleguna Isso é perfeitamente legal. Eu não quero retornar uma referência ou ponteiro para uma variável local, mas um valor. A função foopode, por exemplo parecido: int foo(){return 1;}.
precisa saber é
9
As referências do @aleguna a temporários executam a extensão da vida útil, assim como no C ++ 98.
ecatmur
5
A extensão vitalícia do @aleguna funciona apenas com temporários locais, não com funções que retornam referências. Veja stackoverflow.com/a/2784304/567292
ecatmur

Respostas:

232

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 constreferê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 forloop baseado em intervalo . Veja minha outra pergunta para mais detalhes.

Se você usar std::forwardsua 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_elsewhereextrair 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 varcoragem de nós mesmos - pode muito bem ser um valor ou até uma const. No entanto, podemos std::forwardfazê-lo para outras funções que podem devastar totalmente seu interior. Assim que fizermos isso, devemos considerar varum estado inválido.

Agora vamos aplicar isso ao caso de auto&& var = foo();, como indicado na sua pergunta, em que foo retorna a Tpor valor. Nesse caso, sabemos com certeza que o tipo de varserá deduzido como T&&. Como sabemos com certeza que é um valor, não precisamos std::forwardda permissão para roubar seus recursos. Nesse caso específico, sabendo que fooretorna 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_lvaluepode 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_vectoratravés do parâmetro template de foo.

Quando chamamos foo<std::vector<int>>, get_vectorretornará global_vecpor valor, o que fornece uma expressão rvalue. Como alternativa, quando chamamos foo<std::vector<int>&>, get_vectorretornará global_vecpor 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
}
Joseph Mansfield
fonte
2
Não podemos simplesmente dizer a T vec = get_vector<T>();função interna foo? Ou eu estou simplificando-o a um nível absurdo :)
Asterisk
@Asterisk Não bcoz T vec só pode ser atribuído a lValue em caso de std :: vector <int &> e se T é std :: vector <int>, em seguida, nós estaremos usando chamada por valor que é ineficiente
Kapil
1
auto e me dá o mesmo resultado. Estou usando o MSVC 2015. E o GCC produz um erro.
Sergey Podobry
Aqui, estou usando o MSVC 2015, auto e dá os mesmos resultados que os de auto &&.
Kehe CAI 13/03/18
Por que é int i; auto && j = i; permitido mas int i; int && j = i; não é ?
SeventhSon84 29/04
14

Primeiro, recomendo a leitura desta resposta como uma leitura lateral para uma explicação passo a passo sobre como funciona a dedução de argumentos de modelo para referências universais.

Isso significa que estamos autorizados a roubar os recursos var?

Não necessariamente. E se, foo()de repente, retornasse uma referência ou você alterasse a chamada, mas se esquecesse de atualizar o uso de var? Ou se você estiver no código genérico e o tipo de retorno de foo()pode mudar, dependendo dos seus parâmetros?

Pense auto&&em ser exatamente o mesmo que o T&&in template<class T> void f(T&& v);, porque é (quase ) exatamente isso. O que você faz com referências universais em funções, quando precisa transmiti-las ou usá-las de alguma forma? Você usa std::forward<T>(v)para recuperar a categoria de valor original. Se era um lvalue antes de ser passado para sua função, ele permanece um lvalue após a passagem std::forward. Se fosse um rvalue, ele se tornará um rvalue novamente (lembre-se, uma referência nomeada rvalue é um lvalue).

Então, como você usa varcorretamente de maneira genérica? Use std::forward<decltype(var)>(var). Isso funcionará exatamente da mesma forma que std::forward<T>(v)no modelo de função acima. Se varfor a T&&, você receberá um valor de volta e, se for T&, receberá um valor de volta.

Então, voltando ao tópico: O que auto&& v = f();e std::forward<decltype(v)>(v)em uma base de código nos dizem? Eles nos dizem que vserão adquiridos e repassados ​​da maneira mais eficiente. Lembre-se, porém, que depois de ter encaminhado essa variável, é possível que ela seja movida de, portanto, seria incorreto usá-la ainda mais sem redefini-la.

Pessoalmente, uso auto&&no código genérico quando preciso de uma variável modificável . O encaminhamento perfeito de um rvalue está sendo modificado, pois a operação de movimentação potencialmente rouba suas entranhas. Se eu apenas quero ser preguiçoso (ou seja, não soletrar o nome do tipo, mesmo que eu saiba) e não precisar modificar (por exemplo, ao imprimir apenas elementos de um intervalo), eu continuarei auto const&.


autoé na medida diferente que auto v = {1,2,3};vai fazer vum std::initializer_list, enquanto f({1,2,3})será um fracasso dedução.

Xeo
fonte
Na primeira parte da sua resposta: quero dizer que, se foo()retornar um tipo de valor T, var(esta expressão) será um lvalue e seu tipo (dessa expressão) será uma referência de rvalue para T(ie T&&).
precisa saber é
@ MWid: faz sentido, removeu a primeira parte.
Xeo 5/11/12
3

Considere algum tipo Tque tenha um construtor de movimento e assuma

T t( foo() );

usa esse construtor de movimento.

Agora, vamos usar uma referência intermediária para capturar o retorno de foo:

auto const &ref = foo();

isso exclui o uso do construtor move, então o valor de retorno terá que ser copiado em vez de movido (mesmo se usarmos std::moveaqui, na verdade não podemos mover através de uma const ref)

T t(std::move(ref));   // invokes T::T(T const&)

No entanto, se usarmos

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

o construtor de movimentação ainda está disponível.


E para resolver suas outras perguntas:

... Existem situações razoáveis ​​em que você deve usar auto && para informar algo ao leitor do seu código ...

A primeira coisa, como diz Xeo, é basicamente passar o X da maneira mais eficiente possível , seja qual for o tipo X. Portanto, ver o código que usa auto&&internamente deve comunicar que ele usará a semântica de movimentação internamente, quando apropriado.

... como você faz quando retorna um unique_ptr <> para dizer que possui propriedade exclusiva ...

Quando um modelo de função usa um argumento do tipo T&&, está dizendo que pode mover o objeto que você passa. Retornar unique_ptrexplicitamente dá propriedade ao chamador; aceitar T&&pode remover a propriedade do chamador (se existir um movedor, etc.).

Sem utilidade
fonte
2
Não sei se o seu segundo exemplo é válido. Você não precisa de um encaminhamento perfeito para chamar o construtor de movimentação?
3
Isto está errado. Nos dois casos, o construtor de cópia é chamado, desde refe rvrefsão ambos os valores. Se você quiser o construtor de movimentação, precisará escrever T t(std::move(rvref)).
precisa saber é
Você quis dizer const ref no seu primeiro exemplo auto const &:?
PiotrNycz
@ aleguna - você e MWid estão certos, obrigado. Eu consertei minha resposta.
Inutil inútil
1
@ Useless Você está certo. Mas isso não responde à minha pergunta. Quando você usa auto&&e o que diz ao leitor do seu código usando auto&&?
precisa saber é
-3

A auto &&sintaxe usa dois novos recursos do C ++ 11:

  1. A autoparte permite que o compilador deduza o tipo com base no contexto (o valor de retorno neste caso). Este é, sem quaisquer qualificações de referência (o que lhe permite especificar se deseja T, T &ou T &&para um tipo deduzida T).

  2. A &&é a nova semântica de movimento. Uma semântica de suporte ao tipo implementa um construtor T(T && other)que move o conteúdo da melhor maneira possível. Isso permite que um objeto troque a representação interna em vez de executar uma cópia detalhada.

Isso permite que você tenha algo como:

std::vector<std::string> foo();

Assim:

auto var = foo();

executará uma cópia do vetor retornado (caro), mas:

auto &&var = foo();

trocará a representação interna do vetor (o vetor de fooe o vetor vazio de var), portanto será mais rápido.

Isso é usado na nova sintaxe de loop for:

for (auto &item : foo())
    std::cout << item << std::endl;

Onde o loop for retém um auto &&valor de retorno fooe itemé uma referência para cada valor em foo.

reece
fonte
Isto está incorreto. auto&&não moverá nada, apenas fará uma referência. Seja uma referência lvalue ou rvalue, depende da expressão usada para inicializá-la.
Joseph Mansfield
Em ambos os casos, o construtor movimento ser chamado, uma vez std::vectore std::stringsão moveconstructible. Isso não tem nada a ver com o tipo de var.
MWid
1
@MWid: Na verdade, a chamada para o construtor de copiar / mover também pode ser totalmente eliminada com o RVO.
Matthieu M.
@MatthieuM. Você está certo. Mas acho que no exemplo acima, o copy cnstructor nunca será chamado, uma vez que tudo é passível de construção.
precisa saber é
1
@ MWID: meu argumento era que mesmo o construtor de movimento pode ser elidido. Os trunfos da Elision se movem (é mais barato).
amigos estão