2015-07-11 11 views
5

Se si guarda get, la funzione di supporto per std::tuple, si noterà il seguente overload:Perché fa arrivare aiutante di std :: riferimento rvalue ritorno tupla invece di valore

template< std::size_t I, class... Types > 
constexpr std::tuple_element_t<I, tuple<Types...> >&& 
get(tuple<Types...>&& t); 

In altre parole, restituisce un riferimento di rvalue quando la tupla di input è un riferimento di rvalue stesso. Perché non restituire in base al valore, chiamando move nel corpo della funzione? Il mio argomento è il seguente: il ritorno di get sarà legato a un riferimento oa un valore (potrebbe non essere associato a nulla, suppongo, ma questo non dovrebbe essere un caso d'uso comune). Se è legato a un valore, si verificherà comunque una costruzione di movimento. Quindi non perdi nulla restituendo valore. Se ci si lega a un riferimento, quindi la restituzione di un riferimento rvalue può effettivamente essere non sicura. Per mostrare un esempio:

struct Hello { 
    Hello() { 
    std::cerr << "Constructed at : " << this << std::endl; 
    } 

    ~Hello() { 
    std::cerr << "Destructed at : " << this << std::endl; 
    } 

    double m_double; 
}; 

struct foo { 
    Hello m_hello; 
    Hello && get() && { return std::move(m_hello); } 
}; 

int main() { 
    const Hello & x = foo().get(); 
    std::cerr << x.m_double; 
} 

Quando viene eseguito, questo programma stampa:

Constructed at : 0x7ffc0e12cdc0 
Destructed at : 0x7ffc0e12cdc0 
0 

In altre parole, x è un riferimento immediatamente penzoloni. Se invece hai appena scritto foo in questo modo:

struct foo { 
    Hello m_hello; 
    Hello get() && { return std::move(m_hello); } 
}; 

Questo problema non si verificherebbe. Inoltre, se si quindi utilizzare foo in questo modo:

Hello x(foo().get()); 

Non sembra che ci sia alcun overhead aggiuntivo se si ritorna per valore, o riferimento rvalue. Ho testato il codice in questo modo e sembra che eseguirà in modo coerente solo una singola costruzione di movimento. Per esempio. se posso aggiungere un membro:

Hello(Hello &&) { std::cerr << "Moved" << std::endl; } 

E io costruisco x come sopra, il mio programma di stampa solo "spostato" una volta indipendentemente dal fatto che torno per valore o riferimento rvalue.

C'è una buona ragione che mi manchi o è una svista?

Nota: c'è una buona domanda correlata qui: Return value or rvalue reference?. Sembra che il ritorno di valore sia generalmente preferibile in questa situazione, ma il fatto che si presenti nell'STL mi fa incuriosire se l'AWL abbia ignorato questo ragionamento, o se hanno motivi speciali che potrebbero non essere applicabili. generalmente.

Modifica: Qualcuno ha suggerito che questa domanda sia una copia di Is there any case where a return of a RValue Reference (&&) is useful?. Questo non è il caso; questa risposta suggerisce il ritorno per riferimento di rvalue come metodo per elidere la copia dei membri dei dati. Come discusso in dettaglio sopra, la copiatura verrà eliminata se si restituisce per valore o valore di riferimento, a condizione che si chiami prima move.

+0

Questo non è un duplicato in remoto. La mia risposta esplicitamente e in dettaglio discute su come il ritorno di valore e la chiamata a std :: move anticipatamente ottengano lo stesso effetto dello spostamento da un membro di dati. La mia domanda è legata alle differenze nei due approcci. La domanda che ho citato è più correlata alla mia domanda rispetto a quella che hai citato. Si prega di leggere più attentamente prima di correre a etichettare qualcosa di duplicato. –

+0

se si restituisce per valore - si crea una copia. se lo sposti, ne sposti una copia. se si restituisce un valore-riferimento-r, è possibile spostarlo, quindi l'oggetto originale viene spostato o non gestirlo con le semoventi move, quindi si comporterà come riferimento regolare. il punto è che se si restituisce un riferimento al valore r non si forza la copia. –

+0

Perché richiedere la costruibilità del movimento? – Columbo

risposta

4

L'esempio di come questo può essere utilizzato per creare un riferimento ciondolante è molto interessante, ma è importante imparare la lezione corretta dall'esempio.

Consideriamo un esempio molto più semplice, che non ha alcun && ovunque:

const int &x = vector<int>(1) .front(); 

.front() restituisce un & -Referenza al primo elemento del nuovo vettore costruito. Il vettore viene immediatamente distrutto, ovviamente, e ti rimane un riferimento ciondolante.

La lezione da apprendere è che l'utilizzo di un riferimento const non estende, in generale, la durata. Si estende la durata di non riferimento. Se il lato destro di = è un riferimento, devi assumerti la responsabilità della vita da solo.

Questo è sempre stato il caso, quindi non avrebbe senso per tuple::get fare qualcosa di diverso. tuple::get è consentito restituire un riferimento, proprio come è sempre stato vector::front.

Si parla di spostare e copiare i costruttori e di velocità. La soluzione più veloce è quella di non usare costruttori di alcun tipo. Immaginare una funzione per concatenare due vettori:

vector<int> concat(const vector<int> &l_, const vector<int> &r) { 
    vector<int> l(l_); 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

Ciò consentirebbe un ottimizzato sovraccarico supplementare:

vector<int>&& concat(vector<int>&& l, const vector<int> &r) { 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

Questa ottimizzazione mantiene il numero delle costruzioni al minimo

vector<int> a{1,2,3}; 
    vector<int> b{3,4,5}; 
    vector<int> c = concat(
    concat(
     concat(
      concat(vector<int>(), a) 
     , b) 
     , a 
    , b); 

L'ultima riga , con quattro chiamate a concat, avrà solo due costruzioni: il valore iniziale (vector<int>()) e il movimento-costrutto in c. È possibile avere 100 chiamate nidificate a concat lì, senza ulteriori costruzioni.

Quindi, il ritorno da && può essere più veloce. Perché, sì, le mosse sono più veloci delle copie, ma è ancora più veloce se puoi evitare entrambe.

In breve, è fatto per la velocità. Prendi in considerazione l'uso di una serie nidificata di get su una tupla tuple-within-a-tuple-within-a-tuple. Inoltre, gli consente di lavorare con tipi che non hanno né copia né mosso costruttori.

E questo non introduce nuovi rischi per quanto riguarda la durata. Il "problema" vector<int>().front() non è nuovo.

+2

Ho bisogno di passare un po 'più di tempo con il tuo rispondi per assorbirlo completamente. Nota che ironicamente, il tuo esempio con vector.front non sarebbe generoso se ci fosse un sovraccarico T front() &&. –

+2

Ho pensato di dirlo. Sì, il comitato dovrebbe inserire '&' e '&&' in tutto lo standard, proprio come il tuo esempio nel tuo commento. Ma immagino che ciò romperebbe la compatibilità. Inoltre, forse tutti i metodi dovrebbero essere '&' di default, ma anche quello sarebbe un cambiamento drammatico abbastanza significativo nella lingua. –

+0

Per generalizzare, se una funzione restituisce uno degli argomenti, o un sottooggetto di un argomento, e quell'argomento è '&&', non vedo alcuna ragione per non tornare sempre da '&&'. Sto andando troppo lontano qui? –

Problemi correlati