Qual è il modo corretto di utilizzare la gamma basata su C ++ 11?


212

Qual è il modo corretto di utilizzare la gamma basata su C ++ 11 for?

Quale sintassi dovrebbe essere usata? for (auto elem : container)o for (auto& elem : container)oppure for (const auto& elem : container)? O qualcun altro?


6
La stessa considerazione vale per gli argomenti delle funzioni.
Maxim Egorushkin,

3
In realtà, questo ha poco a che fare con il range-based per. Lo stesso si può dire di qualsiasi auto (const)(&) x = <expr>;.
Matthieu M.

2
@MatthieuM: questo ha molto a che fare con il range-based per, ovviamente! Considera un principiante che vede diverse sintassi e non può scegliere quale forma usare. Lo scopo di "Domande e risposte" era cercare di far luce e spiegare le differenze di alcuni casi (e discutere i casi che vengono compilati bene ma che sono in qualche modo inefficienti a causa di inutili copie approfondite, ecc.).
Mr.C64,

2
@ Mr.C64: Per quanto mi riguarda, questo ha più a che fare con auto, in generale, che con range-based per; puoi usare perfettamente il range-based per senza nessuno auto! for (int i: v) {}va benissimo. Naturalmente, la maggior parte dei punti che sollevi nella tua risposta potrebbe avere più a che fare con il tipo che con auto... ma dalla domanda non è chiaro dove si trovi il punto di dolore. Personalmente, mi batterei per rimuovere autodalla domanda; o forse rendere esplicito che se si utilizza autoo si nomina esplicitamente il tipo, la domanda è focalizzata sul valore / riferimento.
Matthieu M.

1
@MatthieuM .: Sono aperto a cambiare il titolo o modificare la domanda in qualche modo che possa renderli più chiari ... Ancora una volta, il mio obiettivo era discutere diverse opzioni per la sintassi basata su intervallo (mostrando il codice che compila ma è inefficiente, codice che non riesce a compilare, ecc.) e che cerca di offrire una guida a qualcuno (soprattutto a livello principiante) avvicinandosi alla gamma C ++ 11 per i loop.
Mr.C64,

Risposte:


390

Iniziamo a distinguere tra l' osservazione degli elementi nel contenitore e la loro modifica in atto.

Osservando gli elementi

Consideriamo un semplice esempio:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

Il codice sopra stampa gli elementi intnel vector:

1 3 5 7 9

Consideriamo ora un altro caso, in cui gli elementi vettoriali non sono solo interi semplici, ma istanze di una classe più complessa, con costruttore di copie personalizzato, ecc.

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}

    X(int data)
        : m_data(data)
    {}

    ~X() 
    {}

    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }

    int Get() const
    {
        return m_data;
    }

private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

Se utilizziamo la for (auto x : v) {...}sintassi sopra con questa nuova classe:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

l'output è simile a:

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Come può essere letto dall'output, le chiamate del costruttore della copia vengono effettuate durante l'intervallo per le iterazioni di loop.
Questo perché stiamo acquisendo gli elementi dal contenitore in base al valore (la auto xparte in for (auto x : v)).

Questo è un codice inefficiente , ad esempio, se questi elementi sono istanze di std::string, è possibile eseguire allocazioni di memoria heap, con viaggi costosi al gestore della memoria, ecc. Ciò è inutile se vogliamo solo osservare gli elementi in un contenitore.

Pertanto, è disponibile una sintassi migliore: acquisizione per constriferimento , ovvero const auto&:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Ora l'output è:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Senza alcuna chiamata falsa (e potenzialmente costosa) al costruttore di copie.

Così, quando osservando gli elementi in un contenitore (ad esempio, per l'accesso in sola lettura), la seguente sintassi è bene per semplici a basso costo-per-copia tipo, come int, double, ecc .:

for (auto elem : container) 

Altrimenti, catturare per constriferimento è meglio nel caso generale , per evitare inutili (e potenzialmente costose) chiamate dal costruttore di copie:

for (const auto& elem : container) 

Modifica degli elementi nel contenitore

Se vogliamo modificare gli elementi in un contenitore usando range-based for, quanto sopra for (auto elem : container)e le for (const auto& elem : container) sintassi sono sbagliate.

Infatti, nel primo caso, elemarchivia una copia dell'elemento originale, quindi le modifiche apportate ad esso vengono semplicemente perse e non memorizzate in modo persistente nel contenitore, ad esempio:

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

L'output è solo la sequenza iniziale:

1 3 5 7 9

Invece, un tentativo di utilizzare for (const auto& x : v)non riesce a compilare.

g ++ genera un messaggio di errore simile al seguente:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

L'approccio corretto in questo caso è l'acquisizione per non constriferimento:

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

L'output è (come previsto):

10 30 50 70 90

Questa for (auto& elem : container)sintassi funziona anche per tipi più complessi, ad esempio considerando un vector<string>:

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";

// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';

l'output è:

Hi Bob! Hi Jeff! Hi Connie!

Il caso speciale di iteratori proxy

Supponiamo di avere un vector<bool>e vogliamo invertire lo stato booleano logico dei suoi elementi, usando la sintassi sopra:

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

Il codice sopra riportato non riesce a compilare.

g ++ genera un messaggio di errore simile al seguente:

TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^

Il problema è che il std::vectormodello è specializzato per bool, con un'implementazione che comprime le bools per ottimizzare lo spazio (ogni valore booleano è memorizzato in un bit, otto bit "booleani" in un byte).

Per questo motivo (poiché non è possibile restituire un riferimento a un singolo bit), vector<bool>utilizza un cosiddetto modello "proxy iteratore" . Un "iteratore proxy" è un iteratore che, se non referenziato, non produce un normale bool &, ma restituisce (in base al valore) un oggetto temporaneo , che è una classe proxy convertibile inbool . (Vedi anche questa domanda e le relative risposte qui su StackOverflow.)

Per modificare sul posto gli elementi di vector<bool>, è necessario utilizzare un nuovo tipo di sintassi (usando auto&&):

for (auto&& x : v)
    x = !x;

Il seguente codice funziona bene:

vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';

e uscite:

false true true false

Si noti che il for (auto&& elem : container) sintassi funziona anche negli altri casi di iteratori ordinari (non proxy) (ad esempio per a vector<int>o a vector<string>).

(Come nota a margine, la summenzionata sintassi "osservativa" di for (const auto& elem : container)funziona bene anche per il caso iteratore proxy.)

Sommario

La discussione di cui sopra può essere sintetizzata nelle seguenti linee guida:

  1. Per osservare gli elementi, utilizzare la sintassi seguente:

    for (const auto& elem : container)    // capture by const reference
    • Se gli oggetti sono economici da copiare (come ints, doubles, ecc.), È possibile utilizzare una forma leggermente semplificata:

      for (auto elem : container)    // capture by value
  2. Per modificare gli elementi in atto, utilizzare:

    for (auto& elem : container)    // capture by (non-const) reference
    • Se il contenitore utilizza "iteratori proxy" (come std::vector<bool>), utilizzare:

      for (auto&& elem : container)    // capture by &&

Naturalmente, se è necessario creare una copia locale dell'elemento all'interno del corpo del loop, acquisendo per valore (for (auto elem : container) ) è una buona scelta.


Note aggiuntive sul codice generico

Nel codice generico , dal momento che non possiamo fare ipotesi sul fatto che il tipo generico Tsia economico da copiare, in modalità di osservazione è sicuro usare sempre for (const auto& elem : container).
(Ciò non attiverà copie inutili potenzialmente costose, funzionerà bene anche per tipi da copiare a basso costo inte anche per contenitori che utilizzano proxy-iteratori, comestd::vector<bool> .)

Inoltre, in modalità di modifica , se vogliamo che il codice generico funzioni anche in caso di iteratori proxy, l'opzione migliore è for (auto&& elem : container).
(Funzionerà perfettamente anche per i contenitori che utilizzano normali iteratori non proxy, come std::vector<int>o std::vector<string>.)

Pertanto, nel codice generico , è possibile fornire le seguenti linee guida:

  1. Per osservare gli elementi, utilizzare:

    for (const auto& elem : container)
  2. Per modificare gli elementi in atto, utilizzare:

    for (auto&& elem : container)

7
Nessun consiglio per contesti generici? :(
R. Martinho Fernandes,

11
Perché non usare sempre auto&&? C'è un const auto&&?
Martin Ba

1
Immagino che ti manchi il caso in cui hai davvero bisogno di una copia all'interno del ciclo?
juanchopanza,

6
"Se utilizzato dal contenitore 'iteratori proxy'" - e si sa che usa "iteratori proxy" (che potrebbe non essere il caso in codice generico). Quindi penso che il meglio sia davvero auto&&, poiché copre auto&ugualmente bene.
Christian Rau,

5
Grazie, è stata davvero un'ottima "introduzione al corso crash" della sintassi e alcuni suggerimenti per la gamma basata su, per un programmatore C #. +1.
AndrewJacksonZA,

17

Non esiste un modo corretto di utilizzare for (auto elem : container), o for (auto& elem : container)o for (const auto& elem : container). Esprimi semplicemente quello che vuoi.

Vorrei approfondire questo. Facciamo una passeggiata.

for (auto elem : container) ...

Questo è lo zucchero sintattico per:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Puoi usarlo se il tuo contenitore contiene elementi che sono economici da copiare.

for (auto& elem : container) ...

Questo è lo zucchero sintattico per:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Usalo quando vuoi scrivere direttamente sugli elementi nel contenitore, per esempio.

for (const auto& elem : container) ...

Questo è lo zucchero sintattico per:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Come dice il commento, solo per la lettura. E questo è tutto, tutto è "corretto" se usato correttamente.


2
Intendevo fornire alcune indicazioni, compilando codici di esempio (ma essendo inefficienti) o non riuscendo a compilare e spiegando il perché, e provando a proporre alcune soluzioni.
Mr.C64,

2
@ Mr.C64 Oh, mi dispiace - ho appena notato che questa è una di quelle domande tipo FAQ. Sono nuovo di questo sito. Scuse! La tua risposta è fantastica, l'ho votata a fondo, ma volevo anche fornire una versione più concisa per coloro che ne vogliono sapere . Spero di non intromettermi.

1
@ Mr.C64 qual è il problema con OP che risponde anche alla domanda? È solo un'altra, valida risposta.
mfontanini,

1
@mfontanini: non c'è assolutamente alcun problema se qualcuno pubblica qualche risposta, anche meglio della mia. Lo scopo finale è quello di dare un contributo di qualità alla comunità (specialmente per i principianti che possono sentirsi un po 'persi di fronte a diverse sintassi e diverse opzioni che offre C ++).
Mr.C64,

4

Il mezzo corretto è sempre

for(auto&& elem : container)

Ciò garantirà la conservazione di tutta la semantica.


6
Ma cosa succede se il contenitore restituisce solo riferimenti modificabili e voglio chiarire che non desidero modificarli nel ciclo? Non dovrei quindi usare auto const &per chiarire il mio intento?
RedX

@RedX: che cos'è un "riferimento modificabile"?
Razze di leggerezza in orbita

2
@RedX: i riferimenti non sono mai conste non sono mai mutabili. Comunque, la mia risposta è sì, lo farei .
Razze di leggerezza in orbita

4
Sebbene possa funzionare, ritengo che si tratti di un consiglio scadente rispetto all'approccio più sfumato e ponderato fornito dalla risposta eccellente e completa di Mr.C64 fornita sopra. Ridurre al minimo comune denominatore non serve a C ++.
Jack Aidley,

6
Questa proposta di evoluzione linguistica concorda con questa risposta "povera": open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte,
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.