Lo standard C++11 ha introdotto un nuovo costrutto per il controllo di flusso che prende il nome di range-based for loop (ciclo for su sequenze).
Da un punto di vista semantico, questo nuovo costrutto è del tutto equivalente al tradizionale ciclo for. Tuttavia, soprattutto nel caso in cui il ciclo sia basato sull'uso di iteratori, la nuova sintassi risulta più leggibile e quasi auto esplicativa.
Si consideri il listato seguente in cui gli elementi di un container di tipo std::vector<int>
vengono stampati a schermo due volte, la prima mediante un ciclo for
tradizione e la seconda mediante un ciclo range-based.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = {0, 1, 2, 3, 4, 5};
// ciclo for tradizionale
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it)
{
std::cout << *it << "\n";
}
// range-based for
for (int n : v)
{
std::cout << n << "\n";
}
return 0;
}
Un ciclo for tradizionale richiede l'inizializzazione della variabile iterativa it, la definizione esplicita della condizione di uscita e l'incremento della variabile iteratore. Inoltre, la variabile iterativa, sia essa un puntatore o un iteratore, come in questo caso, deve essere dereferenziata per consentire l'accesso all'elemento desiderato nel corpo del ciclo.
La sintassi del ciclo basato su intervalli è invece nettamente più snella, e si compone di tre elementi principali:
- Range-declaration: la dichiarazione di una variabile avente lo stesso tipo degli elementi dell'intervallo, in questo caso
int
. La scelta del nome è del tutto arbitraria, soggetta alle stesse regole che si applicano per la definizione di identificatori in C++. - Range-expression: una espressione, anche un valore temporaneo, il cui valore sia una sequenza di elementi. Lo standard identifica come tali tutti gli oggetti per cui sono definiti dei metodi membro
begin()
eend()
, ad esempio le classi container, oppure cui siano applicabili le funzionistd::begin()
edstd::end()
. Ciò rende possibile usare questo costrutto anche su array semplici. - Il corpo del ciclo, in cui possono apparire le istruzioni
break
econtinue
.
Il compilatore di fatto traduce un range-based for loop in un ciclo tradizionale, e segnala eventuali errori qualora i requisiti su range-declaration e range-expression non siano soddisfatti.
Si noti anche che è possibile usare lo specificatore auto nel contesto di una range-declaration, per brevità.
Copia di oggetti e innovazioni dello standard C++17
L'uso dei metodi std::begin()
ed std::end()
per l'implementazione del costrutto range-based for loop può implicare che gli elementi della sequenza siano copiati, qualora per la classe dell'elemento sia propriamente definita la semantica della copia. Per cicli che accedono gli elementi in sola lettura, ciò può tradursi in un decremento delle performance, soprattutto nel caso di classi complesse.
In questi casi, per ovviare a questo inconveniente sarebbe sufficiente usare gli omologhi metodi std::cbegin()
ed std::cend()
che di fatto restituiscono iteratori const-qualified. Tuttavia, questa alternativa non è contemplata nell'implementazione del costrutto.
Lo standard C++17 ha introdotto la funzione di utilità std::as_const
, la quale consente di trattare la sequenza come una const reference, inibendo di fatto la copia. Ciò si traduce nell'accesso agli elementi del ciclo mediante puntatori a valori costanti, come mostrato nel listato seguente:
#include <iostream>
#include <utility> // contiene std::as_const a partire da C++17
#include <vector>
int main()
{
std::vector<int> v = {0, 1, 2, 3, 4, 5};
// range-based for
for (auto& n : std::as_const(v))
{
std::cout << n << "\n";
}
return 0;
}
Nel caso di sequenze passate come const reference, verranno invocati i sovraccarichi begin() const
ed end() const
, che sono esattamente equivalenti a cbegin()
e cend()
, come da prescrizione dello standard. Inoltre, poiché l'implementazione della funzione std::as_const
è semplice, tale espediente è facilmente applicabile anche quando lo standard di riferimento è antecedente a C++17.
Limitazioni
I cicli basati su range risultano effettivamente più leggibili e manutenibili di un ciclo for
tradizionale in molti casi. Tuttavia, come vedremo nelle prossime lezioni, la Standard Template Library predispone alcune funzioni di utilità che consentono di ottenere un comportamento equivalente a quello di questo costrutto, con il vantaggio di essere più adattabili a casi particolari rispetto le normali istruzioni di controllo di flusso.