"Per ogni elemento, fai questo"
Il classico ciclo for con contatore è ottimo quando hai un indice da far avanzare. Ma il più delle volte l'indice non ti interessa: vuoi solo toccare ogni elemento di un container. Scrivere for (int i = 0; i < v.size(); i++) per farlo è verboso, e i è a un errore di uno dal leggere fuori dai limiti.
C++11 ha aggiunto il ciclo for basato su range proprio per questo. Dai un nome a una variabile, indichi un container e il ciclo percorre ogni elemento al posto tuo:
Niente indice, niente .size(), nessun limite da sbagliare. Leggilo come "per ogni s in scores". Funziona con gli array grezzi, std::vector, std::string, std::map e qualsiasi altra cosa che esponga begin() ed end().
Lascia scegliere il tipo ad auto
Scrivere per esteso il tipo dell'elemento funziona, ma è fragile: cambi il tipo del container e devi cambiare anche ogni ciclo. Abbina il for basato su range ad auto e il compilatore deduce il tipo dell'elemento per te:
C'è però un costo nascosto. Un semplice auto name deduce string e copia ogni elemento in name a ogni passaggio. Per un int non costa nulla; per una string o una struct grande è un'allocazione sprecata a ogni iterazione. La soluzione sono i riferimenti, ed è la prossima cosa da capire.
Modificare sul posto con auto&
Se scrivi auto x, ottieni una copia, quindi assegnare a x cambia la copia, non il container. Guarda questa trappola:
La moltiplicazione non fa nulla, senza avvisare, perché n è una copia usa e getta. Per modificare davvero gli elementi, prendili per riferimento con auto&:
Quella singola & è l'intera differenza tra "guarda ma non toccare" e "modifica sul posto". Se ti chiedi perché le tue modifiche spariscono, il motivo è quasi sempre questo.
Leggere senza copiare: const auto&
Quando devi solo leggere gli elementi ma copiarli è costoso, usa const auto&. Il riferimento evita la copia, e const documenta (e garantisce) che non modificherai nulla:
Una buona regola pratica:
for (auto x : c) // copia: tipi economici (int, char, puntatori)
for (auto& x : c) // modifica: vuoi cambiare gli elementi
for (const auto& x : c) // lettura: tipi pesanti che ti limiti a esaminare
Usa di default const auto& quando leggi e auto& quando scrivi. Ricorri al semplice auto solo per tipi davvero piccoli ed economici da copiare.
Ciclare su map e coppie
Un for basato su range su una std::map ti consegna una std::pair per ogni voce, con .first (la chiave) e .second (il valore). Da C++17 le structured binding ti permettono di spacchettare quella coppia in due variabili con nome direttamente nell'intestazione del ciclo:
[name, age] è molto più chiaro che ripetere ovunque entry.first ed entry.second. Mantieni anche qui const auto&: la chiave di una voce della map è una string, quindi copiare ogni coppia sarebbe uno spreco.
La trappola: non ridimensionare mentre cicli
La trappola più grande è cambiare la dimensione del container mentre un for basato su range lo sta percorrendo. Chiamare push_back, erase, insert o clear può riallocare la memoria sottostante e invalidare gli iteratori interni del ciclo: il risultato è undefined behavior, cioè crash o dati spazzatura, non un errore gentile:
vector<int> v = {1, 2, 3};
for (int x : v) {
v.push_back(x); // UNDEFINED BEHAVIOR: la riallocazione invalida il range
}
Se devi aggiungere o rimuovere elementi durante l'elaborazione, passa a un ciclo for basato su indice o su iteratore e gestisci tu i limiti, oppure costruisci un container separato per il risultato e scambialo alla fine. Due trappole minori della stessa famiglia: non legare mai un for basato su range a un temporaneo che muore subito (for (auto x : makeVector()) va bene, ma for (auto& x : someObj.getTempVector()) può restare pendente), e ricorda che for (auto& c : myString) ti permette di modificare i singoli caratteri sul posto.
Prossimo passo: le funzioni
Il ciclo for basato su range mette ordine nell'iterazione, e le scelte tra auto / auto& / const auto& che hai appena imparato si applicano pari pari a uno degli strumenti più importanti di C++. Nella prossima pagina racchiuderemo la logica in funzioni riutilizzabili, dando al codice un nome, dei parametri e un valore di ritorno così potrai chiamarlo da qualsiasi punto invece di ripeterti.
Domande frequenti
Cos'è un ciclo for basato su range in C++?
Un ciclo for basato su range visita ogni elemento di un container (array, vector, string, map e così via) senza che tu debba gestire un indice o un iteratore. La sintassi è for (auto x : container) { ... }. È stato aggiunto in C++11 ed è il modo più pulito per dire "fai questo per ogni elemento".
Quando usare auto& invece di auto in un ciclo for basato su range?
Usa auto& x quando vuoi modificare gli elementi sul posto, e const auto& x quando li leggi soltanto ma vuoi evitare di copiarli (importante per string, vector o oggetti grandi). Un semplice auto x crea una copia a ogni iterazione: va bene per tipi economici come int, altrimenti è uno spreco.
Si può cambiare la dimensione di un vector dentro un ciclo for basato su range in C++?
No. Chiamare push_back, erase, insert o clear sul container che stai iterando invalida gli iteratori interni del ciclo ed è undefined behavior: può causare un crash o corrompere i dati senza avvisare. Se devi aggiungere o rimuovere elementi durante il ciclo, usa invece un ciclo for basato su indice o su iteratore.