Cosa ci dice Auto &&?


168

Se leggi il codice come

auto&& var = foo();

dove fooè una funzione che ritorna per valore di tipo T. Quindi varè un valore di tipo rvalue riferimento a T. Ma cosa implica questo var? Significa che ci è permesso rubare le risorse di var? Ci sono situazioni ragionevoli in cui dovresti usare auto&&per dire al lettore del tuo codice qualcosa come te quando ritorni un a unique_ptr<>per dire che hai la proprietà esclusiva? E che dire ad esempio di T&&quando Tè di tipo classe?

Voglio solo capire se ci sono altri casi d'uso diversi auto&&da quelli nella programmazione dei template; come quelli discussi negli esempi in questo articolo Riferimenti universali di Scott Meyers.


1
Mi chiedevo la stessa cosa. Capisco come funziona la detrazione del tipo, ma cosa dice il mio codice quando lo utilizzo auto&&? Ho pensato di capire perché un loop basato su range si espande per usare auto&&come esempio, ma non ci sono riuscito. Forse chi risponde può spiegarlo.
Joseph Mansfield,

1
È legale? Voglio dire che instnce di T viene distrutto immediatamente dopo i fooritorni, la memorizzazione di un riferimento di valore suona come UB a ne.

1
@aleguna Questo è perfettamente legale. Non voglio restituire un riferimento o un puntatore a una variabile locale, ma un valore. La funzione foopotrebbe ad esempio guardare come: int foo(){return 1;}.
MWid

9
I riferimenti a temporali di @aleguna eseguono l'estensione di durata, proprio come in C ++ 98.
ecatmur,

5
L'estensione a vita di @aleguna funziona solo con i temporali locali, non con le funzioni che restituiscono riferimenti. Vedere stackoverflow.com/a/2784304/567292
ecatmur

Risposte:


232

Usando auto&& var = <initializer>stai dicendo: accetterò qualsiasi inizializzatore indipendentemente dal fatto che sia un'espressione lvalue o rvalue e conserverò la sua costanza . Questo è in genere utilizzato per l' inoltro (di solito con T&&). Il motivo per cui funziona è perché un "riferimento universale", auto&&o T&&, si legherà a qualsiasi cosa .

Potresti dire, beh, perché non usare solo un const auto&perché si legherà anche a qualcosa? Il problema con l'utilizzo di un constriferimento è che è const! Non sarai in grado di associarlo in seguito a riferimenti non costanti o invocare funzioni membro non contrassegnate const.

Ad esempio, immagina di voler ottenere un std::vector, porta un iteratore al suo primo elemento e modifica in qualche modo il valore indicato da quell'iteratore:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Questo codice verrà compilato correttamente indipendentemente dall'espressione dell'inizializzatore. Le alternative auto&&falliscono nei seguenti modi:

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

Quindi, per questo, auto&&funziona perfettamente! Un esempio di utilizzo auto&&come questo è in un forciclo basato sull'intervallo. Vedi la mia altra domanda per maggiori dettagli.

Se poi usi std::forwardil tuo auto&&riferimento per preservare il fatto che originariamente era un valore lvalue o un valore rvalue, il tuo codice dice: Ora che ho ottenuto il tuo oggetto da un'espressione lvalue o rvalue, voglio preservare qualunque valore originariamente così ho potuto usarlo nel modo più efficiente - questo potrebbe invalidarlo. Come in:

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));

Ciò consente use_it_elsewheredi strappare il fegato per motivi di prestazioni (evitando copie) quando l'inizializzatore originale era un valore modificabile.

Cosa significa se possiamo o quando possiamo rubare risorse var? Bene, dato che la auto&&volontà si lega a qualcosa, non possiamo assolutamente tentare di strapparci le varviscere - potrebbe benissimo essere un valore o addirittura una const. Possiamo tuttavia std::forwardad altre funzioni che possono devastare totalmente la sua parte interna. Non appena lo facciamo, dovremmo considerare vardi essere in uno stato non valido.

Ora applichiamo questo al caso di auto&& var = foo();, come indicato nella tua domanda, in cui foo restituisce un Tvalore. In questo caso sappiamo con certezza che il tipo di varverrà dedotto come T&&. Dato che sappiamo per certo che si tratta di un valore, non abbiamo bisogno std::forwarddel permesso per rubare le sue risorse. In questo caso specifico, sapendo che fooritorna per valore , il lettore dovrebbe semplicemente leggerlo come: sto prendendo un riferimento di valore al temporaneo restituito da foo, così posso felicemente spostarmi da esso.


Come addendum, penso che valga la pena menzionare quando some_expression_that_may_be_rvalue_or_lvaluepotrebbe apparire un'espressione simile , a parte una situazione "bene il tuo codice potrebbe cambiare". Quindi ecco un esempio inventato:

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;
}

Ecco l' get_vector<T>()espressione adorabile che può essere sia un valore che un valore a seconda del tipo generico T. Modifichiamo essenzialmente il tipo di ritorno di get_vectortramite il parametro template di foo.

Quando chiamiamo foo<std::vector<int>>, get_vectortornerà global_vecper valore, che dà un'espressione di valore. In alternativa, quando chiamiamo foo<std::vector<int>&>, get_vectortornerà global_vecper riferimento, risultando in un'espressione lvalue.

Se lo facciamo:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Otteniamo il seguente output, come previsto:

2
1
2
2

Se si dovesse cambiare il auto&&nel codice per qualsiasi auto, auto&, const auto&, o const auto&&allora noi non otteniamo il risultato che vogliamo.


Un modo alternativo per modificare la logica del programma in base all'inizializzazione del auto&&riferimento con un'espressione lvalue o rvalue consiste nell'utilizzare tratti di 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
}

2
Non possiamo semplicemente dire T vec = get_vector<T>();dentro la funzione foo? O lo sto semplificando a un livello assurdo :)
Asterisco,

@Asterisk No bcoz T vec può essere assegnato a lvalue solo in caso di std :: vector <int &> e se T è std :: vector <int> allora useremo call per valore che è inefficiente
Kapil

1
auto & mi dà lo stesso risultato. Sto usando MSVC 2015. E GCC produce un errore.
Sergey Podobry,

Qui sto usando MSVC 2015, auto & dà gli stessi risultati di auto &&.
Kehe CAI

Perché int i; auto && j = i; consentito ma int i; int && j = i; non è ?
SeventhSon84

14

Innanzitutto, raccomando di leggere questa mia risposta come una lettura a margine per una spiegazione dettagliata su come funziona la deduzione dell'argomento modello per i riferimenti universali.

Significa che ci è permesso rubare le risorse di var?

Non necessariamente. Cosa accadrebbe se foo()all'improvviso restituissi un riferimento o cambiassi la chiamata ma dimenticassi di aggiornare l'uso di var? O se hai un codice generico e il tipo di foo()reso potrebbe cambiare a seconda dei tuoi parametri?

Pensa di auto&&essere esattamente lo stesso di T&&in template<class T> void f(T&& v);, perché è (quasi ) esattamente quello. Cosa fai con i riferimenti universali nelle funzioni, quando devi passarli o usarli in qualche modo? Si utilizza std::forward<T>(v)per ripristinare la categoria di valore originale. Se era un valore prima di essere passato alla funzione, rimane un valore dopo essere stato passato std::forward. Se era un valore, diventerà di nuovo un valore (ricorda, un riferimento al valore denominato è un valore).

Quindi, come si usa varcorrettamente in modo generico? Usa std::forward<decltype(var)>(var). std::forward<T>(v)Funzionerà esattamente come nel modello di funzione sopra. Se varè un T&&, otterrai un valore indietro, e se lo è T&, otterrai un valore indietro.

Quindi, tornando all'argomento: cosa ci dicono auto&& v = f();e std::forward<decltype(v)>(v)in una base di codice? Ci dicono che vsaranno acquisiti e trasmessi nel modo più efficiente. Ricorda, tuttavia, che dopo aver inoltrato una tale variabile, è possibile che venga spostato, quindi sarebbe errato usarlo ulteriormente senza ripristinarlo.

Personalmente, io uso auto&&in codice generico, quando ho bisogno di un modifyable variabile. L'avanzamento perfetto di un valore sta modificando, poiché l'operazione di spostamento potenzialmente ruba le viscere. Se voglio solo essere pigro (cioè non scrivere il nome del tipo anche se lo conosco) e non ho bisogno di modificarlo (ad esempio, quando stampo solo elementi di un intervallo), mi atterrò a auto const&.


autoè così diverso che auto v = {1,2,3};farà vun errore std::initializer_list, mentre f({1,2,3})sarà un fallimento di detrazione.


Nella prima parte della tua risposta: intendo che se foo()restituisce un valore-tipo T, allora var(questa espressione) sarà un valore e il suo tipo (di questa espressione) sarà un riferimento di valore a T(cioè T&&).
MWid

@MWid: ha senso, rimosso la prima parte.
Xeo

3

Prendi in considerazione un tipo Tche ha un costruttore di mosse e assumilo

T t( foo() );

usa quel costruttore di mosse.

Ora, usiamo un riferimento intermedio per acquisire il ritorno da foo:

auto const &ref = foo();

questo esclude l'uso del costruttore di spostamento, quindi il valore restituito dovrà essere copiato anziché spostato (anche se utilizziamo std::movequi, non possiamo effettivamente spostarci attraverso un riferimento const)

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

Tuttavia, se usiamo

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

il costruttore di mosse è ancora disponibile.


E per rispondere ad altre tue domande:

... Ci sono situazioni ragionevoli in cui dovresti usare auto && per dire qualcosa al lettore del tuo codice ...

La prima cosa, come dice Xeo, è essenzialmente che sto passando X nel modo più efficiente possibile , qualunque sia il tipo X. Quindi, vedere il codice che utilizza auto&&internamente dovrebbe comunicare che utilizzerà la semantica di spostamento internamente dove appropriato.

... come fai quando restituisci un unique_ptr <> per dire che hai la proprietà esclusiva ...

Quando un modello di funzione accetta un argomento di tipo T&&, sta dicendo che può spostare l'oggetto in cui si passa. Il ritorno unique_ptrrestituisce esplicitamente la proprietà al chiamante; l'accettazione T&&può rimuovere la proprietà del chiamante (se esiste un gestore di traslochi, ecc.).


2
Non sono sicuro che il tuo secondo esempio sia valido. Non hai bisogno del tuo perfetto inoltro per invocare il costruttore di mosse?

3
Questo è sbagliato. In entrambi i casi il costruttore di copia è chiamata, dal momento che refe rvrefsono entrambi lvalue. Se vuoi il costruttore di mosse, devi scrivere T t(std::move(rvref)).
MWid

Intendevi const ref nel tuo primo esempio auto const &:?
PiotrNycz,

@aleguna - tu e MWid avete ragione, grazie. Ho corretto la mia risposta.
Inutile

1
@Useless Hai ragione. Ma questo non risponde alla mia domanda. Quando usi auto&&e cosa dici al lettore del tuo codice usando auto&&?
MWid

-3

La auto &&sintassi utilizza due nuove funzionalità di C ++ 11:

  1. La autoparte consente al compilatore di dedurre il tipo in base al contesto (il valore restituito in questo caso). Questo è senza alcuna qualifica di riferimento (che consentono di specificare se si desidera T, T &o T &&per un tipo dedotta T).

  2. Il &&è la nuova semantica mossa. Un tipo che supporta la semantica di spostamento implementa un costruttore T(T && other)che sposta in modo ottimale il contenuto nel nuovo tipo. Ciò consente a un oggetto di scambiare la rappresentazione interna invece di eseguire una copia profonda.

Questo ti permette di avere qualcosa come:

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

Così:

auto var = foo();

eseguirà una copia del vettore restituito (costoso), ma:

auto &&var = foo();

cambierà la rappresentazione interna del vettore (il vettore da fooe il vettore vuoto da var), quindi sarà più veloce.

Questo è usato nella nuova sintassi for-loop:

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

Dove il ciclo for tiene un auto &&valore di ritorno da fooed itemè un riferimento a ciascun valore in foo.


Questo non è corretto auto&&non sposterà nulla, farà solo un riferimento. Che si tratti di un riferimento lvalue o rvalue dipende dall'espressione utilizzata per inizializzarlo.
Joseph Mansfield,

In entrambi i casi sarà il costruttore mossa essere chiamato, in quanto std::vectore std::stringsono moveconstructible. Questo non ha nulla a che fare con il tipo di var.
MWid

1
@MWid: In realtà, la chiamata al costruttore di copia / spostamento potrebbe anche essere elusa del tutto con RVO.
Matthieu M.,

@MatthieuM. Hai ragione. Ma penso che nell'esempio sopra, il copistatore non verrà mai chiamato, dato che tutto è mobile.
MWid

1
@MWid: il punto era che anche il costruttore di mosse può essere eluso. Le briscole di elisione si muovono (è più economico).
Matthieu M.,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.