Menu

Ciclo for basato su range in C++: sintassi, auto e riferimenti

Il ciclo for basato su range di C++ spiegato: iterazione pulita su array, vector, stringhe e map, perché usare auto& e const auto&, e le trappole di copie e iteratori invalidati da evitare.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

"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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA