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 int
nel 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 x
parte 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 const
riferimento , 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 const
riferimento è 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, elem
archivia 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 const
riferimento:
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::vector
modello è specializzato per bool
, con un'implementazione che comprime le bool
s 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:
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 int
s, double
s, ecc.), È possibile utilizzare una forma leggermente semplificata:
for (auto elem : container) // capture by value
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 T
sia 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 int
e 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:
Per osservare gli elementi, utilizzare:
for (const auto& elem : container)
Per modificare gli elementi in atto, utilizzare:
for (auto&& elem : container)