🔝 Retour au Sommaire
Itérer sur un conteneur en C++ a longtemps été verbeux et sujet aux erreurs. Prenons un simple parcours de std::vector avec les outils hérités du C et du C++ pré-2011 :
#include <iostream>
#include <vector>
int main() {
std::vector<std::string> fruits = {"pomme", "banane", "cerise"};
// Style C — index manuel
for (std::size_t i = 0; i < fruits.size(); ++i) {
std::cout << fruits[i] << "\n";
}
// Style C++98 — itérateurs explicites
for (std::vector<std::string>::const_iterator it = fruits.begin();
it != fruits.end(); ++it) {
std::cout << *it << "\n";
}
}Les deux formes fonctionnent, mais elles partagent les mêmes défauts. Le code est bruyant : on manipule des indices ou des itérateurs alors qu'on veut simplement accéder à chaque élément. Les risques d'erreur sont réels : un < au lieu de !=, un mauvais type d'index (int au lieu de std::size_t), un itérateur invalidé. Et surtout, l'intention — « pour chaque fruit, faire quelque chose » — est noyée dans la mécanique d'itération.
C++11 introduit une syntaxe de boucle qui exprime directement le parcours d'une plage d'éléments :
for (declaration : expression) {
// corps de la boucle
}L'expression doit être un objet sur lequel on peut appeler begin() et end() (ou pour lequel ces fonctions existent en tant que fonctions libres). La declaration définit la variable qui prend la valeur de chaque élément à chaque itération.
#include <iostream>
#include <vector>
int main() {
std::vector<std::string> fruits = {"pomme", "banane", "cerise"};
for (const auto& fruit : fruits) {
std::cout << fruit << "\n";
}
}Le code est plus court, plus lisible, et ne laisse aucune place aux erreurs d'indexation. L'intention est limpide dès la première lecture.
Le range-based for est du sucre syntaxique. Le compilateur le transforme en une boucle avec itérateurs. En simplifiant légèrement, cette boucle :
for (const auto& fruit : fruits) {
std::cout << fruit << "\n";
}est équivalente à :
{
auto&& __range = fruits;
auto __begin = __range.begin();
auto __end = __range.end();
for (; __begin != __end; ++__begin) {
const auto& fruit = *__begin;
std::cout << fruit << "\n";
}
}Comprendre cette transformation aide à anticiper le comportement dans les cas limites (invalidation d'itérateurs, modification du conteneur pendant le parcours, etc.).
Le choix du type dans la déclaration détermine si l'élément est copié, référencé, ou accessible en lecture seule. Ce choix a un impact direct sur la performance et sur ce qu'il est possible de faire dans le corps de la boucle.
for (auto fruit : fruits) {
fruit += " mûr(e)"; // modifie la copie locale, pas le vecteur
std::cout << fruit << "\n";
}
// Le vecteur 'fruits' est inchangéChaque itération crée une copie de l'élément. C'est acceptable pour des types légers (int, double, char), mais coûteux pour des objets plus lourds comme std::string ou des structures complexes.
std::vector<int> scores = {72, 85, 91, 68};
for (auto& score : scores) {
score += 5; // modifie directement l'élément du vecteur
}
// scores == {77, 90, 96, 73}Aucune copie n'est effectuée. La variable score est un alias vers l'élément réel du conteneur. Toute modification est répercutée.
for (const auto& fruit : fruits) {
std::cout << fruit << "\n";
// fruit += "!"; // ❌ Erreur de compilation — const
}C'est la forme la plus courante et la plus sûre pour un parcours en lecture seule. Elle combine l'absence de copie (performance) et l'impossibilité de modifier l'élément (sécurité).
| Déclaration | Copie ? | Modifiable ? | Usage typique |
|---|---|---|---|
auto x |
Oui | Copie locale uniquement | Types primitifs, besoin d'une copie indépendante |
auto& x |
Non | Oui (modifie le conteneur) | Transformation en place |
const auto& x |
Non | Non | Parcours en lecture seule (cas par défaut) |
💡 Règle pratique — Utilisez
const auto&par défaut. Passez àauto&uniquement quand vous devez modifier les éléments. Utilisezauto(par valeur) uniquement pour les types primitifs ou quand vous avez besoin d'une copie indépendante.
Le range-based for fonctionne avec tout objet qui fournit une paire begin() / end() retournant des itérateurs. En pratique, cela couvre la quasi-totalité des cas courants.
Tous les conteneurs standard sont compatibles : std::vector, std::array, std::list, std::deque, std::set, std::map, std::unordered_map, etc.
#include <map>
#include <iostream>
int main() {
std::map<std::string, int> ages = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (const auto& [nom, age] : ages) { // Structured binding (C++17)
std::cout << nom << " a " << age << " ans\n";
}
}L'exemple ci-dessus utilise les structured bindings (C++17, section 12.1) pour décomposer chaque std::pair<const std::string, int> en deux variables nommées. Sans C++17, on écrirait pair.first et pair.second :
// C++11/14 — sans structured bindings
for (const auto& pair : ages) {
std::cout << pair.first << " a " << pair.second << " ans\n";
}Les tableaux de taille fixe déclarés sur la stack sont pris en charge. Le compilateur connaît leur taille à la compilation et peut générer les bornes automatiquement.
int values[] = {10, 20, 30, 40, 50};
for (int v : values) {
std::cout << v << "\n";
}En revanche, un pointeur vers un tableau (typiquement reçu en paramètre de fonction) ne fonctionne pas : le compilateur a perdu l'information de taille. C'est une des raisons pour lesquelles std::span (C++20, section 13.5) existe.
void process(int* data, std::size_t size) {
// ❌ Erreur — 'data' est un pointeur, pas un tableau
// for (int v : data) { ... }
// ✅ Avec std::span (C++20)
// for (int v : std::span(data, size)) { ... }
// ✅ Boucle classique en attendant
for (std::size_t i = 0; i < size; ++i) {
std::cout << data[i] << "\n";
}
}On peut itérer sur une liste d'initialisation temporaire, ce qui est pratique pour des boucles ponctuelles sur un ensemble de valeurs connues :
for (auto x : {1, 2, 3, 5, 8, 13}) {
std::cout << x << " ";
}
// Sortie : 1 2 3 5 8 13std::string expose begin() et end(), ce qui permet d'itérer caractère par caractère :
std::string mot = "Bonjour";
for (char c : mot) {
std::cout << c << " ";
}
// Sortie : B o n j o u rPour qu'un type personnalisé fonctionne avec le range-based for, il suffit de fournir des fonctions begin() et end() — soit comme méthodes membres, soit comme fonctions libres. Les « itérateurs » retournés doivent supporter au minimum trois opérations : la déréférence (*), l'incrémentation préfixe (++), et la comparaison d'inégalité (!=).
#include <iostream>
class IntRange {
int start_;
int end_;
public:
IntRange(int start, int end) : start_(start), end_(end) {}
struct Iterator {
int current;
int operator*() const { return current; }
Iterator& operator++() { ++current; return *this; }
bool operator!=(const Iterator& other) const {
return current != other.current;
}
};
Iterator begin() const { return {start_}; }
Iterator end() const { return {end_}; }
};
int main() {
for (int n : IntRange(1, 6)) {
std::cout << n << " ";
}
// Sortie : 1 2 3 4 5
}Ce pattern est la base des views et des ranges (C++20, section 15.6), qui généralisent ce concept en pipelines composables.
Le range-based for est la forme recommandée par défaut, mais certaines situations requièrent toujours les boucles classiques.
std::vector<std::string> lignes = read_file("data.txt");
// L'index est nécessaire pour numéroter les lignes
for (std::size_t i = 0; i < lignes.size(); ++i) {
std::cout << (i + 1) << ": " << lignes[i] << "\n";
}💡 En C++23,
std::views::enumeratepermet d'obtenir un index avec un range-basedfor:for (auto [i, val] : std::views::enumerate(vec)). Avant cela, la boucle classique avec index reste la solution la plus claire.
// Parcours inverse
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << "\n";
}
// Parcours avec un pas de 2
for (std::size_t i = 0; i < vec.size(); i += 2) {
std::cout << vec[i] << "\n";
}C++20 propose std::views::reverse pour le parcours inverse dans un range-based for, mais les pas personnalisés nécessitent toujours une boucle classique ou std::views::stride (C++23).
Modifier la taille d'un conteneur pendant un range-based for provoque un comportement indéfini (les itérateurs sont invalidés). Pour supprimer des éléments, on utilise l'idiome erase-remove ou une boucle avec itérateur explicite :
// ✅ Idiome erase-remove (pas de boucle manuelle)
vec.erase(
std::remove_if(vec.begin(), vec.end(),
[](int x) { return x < 0; }),
vec.end()
);
// ✅ En C++20 : std::erase_if (encore plus simple)
std::erase_if(vec, [](int x) { return x < 0; });Le range-based for ne couvre pas les cas où la condition d'arrêt n'est pas liée à un conteneur — lecture de flux, boucle événementielle, attente d'une condition externe :
// Lecture ligne par ligne depuis stdin
std::string line;
while (std::getline(std::cin, line)) {
process(line);
}
// Boucle événementielle
do {
event = poll_event();
handle(event);
} while (event.type != EventType::Quit);Itérer directement sur un temporaire simple est sûr — le compilateur prolonge sa durée de vie jusqu'à la fin de la boucle :
// ✅ Sûr — le temporaire vit jusqu'à la fin du for
for (auto& c : get_string_by_value()) {
std::cout << c;
}Le vrai piège survient quand l'expression range contient un temporaire intermédiaire dont la durée de vie n'est pas prolongée :
// ❌ Comportement indéfini (avant C++23)
// foo() crée un temporaire, .get_items() retourne une référence vers ses données,
// puis le temporaire est détruit avant la première itération
for (auto& item : foo().get_items()) {
process(item);
}Avant C++23, la solution est de stocker le résultat intermédiaire dans une variable nommée :
// ✅ Sûr dans toutes les versions
auto obj = foo();
for (auto& item : obj.get_items()) {
process(item);
}C++23 (P2718R0) a corrigé ce problème en prolongeant la durée de vie de tous les temporaires de l'expression range jusqu'à la fin de la boucle.
std::vector<std::string> logs = get_logs(); // milliers d'entrées
// ❌ Copie chaque std::string à chaque itération
for (auto log : logs) {
std::cout << log << "\n";
}
// ✅ Référence constante — aucune copie
for (const auto& log : logs) {
std::cout << log << "\n";
}Sur un vecteur de 10 000 chaînes, la différence peut se chiffrer en millisecondes. Avec -Wall -Wextra, certains compilateurs ne signalent malheureusement pas ce cas — c'est au développeur d'y penser.
std::vector<int> nums = {1, 2, 3, 4, 5};
// ❌ Comportement indéfini — push_back peut invalider les itérateurs
for (auto n : nums) {
if (n == 3) {
nums.push_back(30);
}
}Toute opération qui invalide les itérateurs (insertion, suppression, réallocation) est interdite pendant un range-based for. Si vous devez modifier la structure du conteneur, utilisez une boucle avec itérateur explicite ou accumulez les modifications pour les appliquer après la boucle.
| Situation | Boucle recommandée |
|---|---|
| Parcourir tous les éléments d'un conteneur | for (const auto& x : conteneur) |
| Modifier chaque élément en place | for (auto& x : conteneur) |
| Besoin d'un index | for classique avec std::size_t |
| Pas d'itération non standard | for classique |
| Parcours inverse | for avec rbegin()/rend() ou std::views::reverse (C++20) |
| Condition d'arrêt dynamique (non liée à un conteneur) | while ou do-while |
| Lecture de flux (fichier, stdin, socket) | while avec std::getline ou opérateur >> |
La section 4.1 est terminée. La section 4.2 aborde un sujet tout aussi fondamental : la déclaration et la définition de fonctions en C++, avec la distinction entre prototype et implémentation, la séparation .h/.cpp, et la règle ODR (One Definition Rule).