Conteneurs séquentiels : vector, deque, list, array
Explorez les conteneurs séquentiels de la STL — vector, deque, list, forward_list et array. Découvrez leurs compromis de performance, dispositions mémoire et règles d'invalidation des itérateurs.
Que sont les conteneurs séquentiels ?
Les conteneurs séquentiels stockent les éléments dans un ordre linéaire strict. La STL fournit cinq conteneurs séquentiels, chacun optimisé pour différents modes d'accès et de modification :
- std::vector — tableau dynamique, mémoire contiguë
- std::deque — file à double extrémité, insertion rapide aux deux bouts
- std::list — liste doublement chaînée
- std::forward_list — liste simplement chaînée
- std::array — tableau de taille fixe, alloué sur la pile
Choisir le bon conteneur est l'une des décisions de performance les plus impactantes que vous prendrez en C++. Les facteurs clés sont le mode d'accès, le mode d'insertion/suppression et la localité de cache.
std::vector — Le choix par défaut
std::vector est le conteneur le plus couramment utilisé. Il stocke les éléments dans un seul bloc de mémoire contiguë, ce qui lui confère d'excellentes performances de cache et un accès aléatoire en O(1). L'ajout d'éléments avec push_back est en O(1) amorti car le vector double sa capacité lorsqu'il est plein.
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums; // empty vector
nums.reserve(100); // pre-allocate for 100 elements (no reallocation)
for (int i = 0; i < 10; ++i) {
nums.push_back(i * i); // amortized O(1)
}
// Size vs capacity
std::cout << "Size: " << nums.size() << '\n'; // 10
std::cout << "Capacity: " << nums.capacity() << '\n'; // >= 100
// Random access — O(1)
std::cout << "Element 3: " << nums[3] << '\n'; // 9
std::cout << "Element 3: " << nums.at(3) << '\n'; // 9 (bounds-checked)
// Erase-remove idiom (pre-C++20)
nums.erase(std::remove_if(nums.begin(), nums.end(),
[](int x) { return x > 50; }),
nums.end());
// C++20: std::erase_if (cleaner)
std::erase_if(nums, [](int x) { return x > 30; });
// Range-based for loop
for (int n : nums) {
std::cout << n << ' ';
}
std::cout << '\n';
}deque, list et forward_list
std::deque offre une insertion et suppression en O(1) aux deux extrémités, mais utilise une disposition mémoire segmentée (tableau de blocs de taille fixe) plutôt qu'une seule allocation contiguë. std::list est une liste doublement chaînée offrant une insertion/suppression en O(1) n'importe où avec un itérateur, au prix d'une mauvaise localité de cache. std::forward_list est une liste simplement chaînée qui utilise moins de mémoire par nœud mais ne supporte que le parcours vers l'avant.
#include <deque>
#include <list>
#include <forward_list>
#include <iostream>
int main() {
// deque — O(1) push_front and push_back
std::deque<int> dq;
dq.push_front(1); // O(1) — vector cannot do this efficiently
dq.push_back(2); // O(1)
dq.push_front(0); // O(1)
// dq: {0, 1, 2}
// list — O(1) insert/erase at any position with iterator
std::list<std::string> names = {"Alice", "Charlie", "Eve"};
auto it = std::next(names.begin()); // points to "Charlie"
names.insert(it, "Bob"); // O(1) insert before "Charlie"
names.erase(it); // O(1) erase "Charlie"
// names: {"Alice", "Bob", "Eve"}
// list has splice — O(1) move of entire sublists
std::list<std::string> more = {"Frank", "Grace"};
names.splice(names.end(), more); // moves all of 'more' into 'names'
// names: {"Alice", "Bob", "Eve", "Frank", "Grace"}
// more is now empty
// forward_list — singly linked, no size(), push_front only
std::forward_list<int> fl = {3, 1, 4, 1, 5};
fl.push_front(0);
fl.sort(); // member sort — O(n log n), stable
fl.unique(); // remove consecutive duplicates
// fl: {0, 1, 3, 4, 5}
for (int v : fl) std::cout << v << ' ';
std::cout << '\n';
}std::array — Taille fixe, zéro surcoût
std::array encapsule un tableau C classique dans une vraie classe, fournissant .size(), .at(), des itérateurs et la compatibilité avec tous les algorithmes STL — sans aucun surcoût par rapport à un T[N] brut.
#include <array>
#include <algorithm>
#include <iostream>
int main() {
std::array<int, 5> arr = {5, 2, 8, 1, 9};
std::sort(arr.begin(), arr.end()); // works with STL algorithms
std::cout << "Size: " << arr.size() << '\n'; // constexpr — known at compile time
std::cout << "Front: " << arr.front() << '\n'; // 1
std::cout << "Back: " << arr.back() << '\n'; // 9
// Bounds-checked access
try {
arr.at(10); // throws std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "Out of range: " << e.what() << '\n';
}
// Compile-time size via std::tuple_size
constexpr std::size_t n = std::tuple_size_v<decltype(arr)>; // 5
static_assert(n == 5);
}Choisir le bon conteneur séquentiel
Utilisez ce guide de décision :
Utilisez std::vector (par défaut) lorsque vous avez besoin d'un accès aléatoire, d'une itération optimisée pour le cache, ou d'un push/pop à l'arrière. La grande majorité du code devrait utiliser vector.
Utilisez std::deque lorsque vous avez besoin d'insertions/suppressions fréquentes aux deux extrémités (par ex., une fenêtre glissante, une file BFS).
Utilisez std::list lorsque vous avez besoin d'une insertion/suppression en O(1) au milieu avec un itérateur, ou lorsque vous avez besoin de splice() pour déplacer des sous-listes entre listes sans copie.
Utilisez std::forward_list lorsque la mémoire par nœud est importante et que vous n'avez besoin que du parcours vers l'avant.
Utilisez std::array lorsque la taille est connue à la compilation et ne changera jamais.
Note sur les performances : En raison des effets de cache du CPU, std::vector surpasse souvent std::list même pour les insertions au milieu sur le matériel moderne, jusqu'à des tailles étonnamment grandes (milliers d'éléments). Mesurez toujours avant de choisir une liste chaînée.
Chaque conteneur a des règles différentes pour l'invalidation des itérateurs, pointeurs et références :
vector — Une insertion qui déclenche une réallocation invalide tous les itérateurs. Une insertion sans réallocation invalide les itérateurs situés au point d'insertion ou après. Une suppression invalide les itérateurs situés au point de suppression ou après.
deque — Une insertion en début ou en fin invalide tous les itérateurs mais pas les références/pointeurs vers les éléments existants. Une insertion au milieu invalide tout. Une suppression en début/fin n'invalide que l'itérateur de l'élément supprimé.
list / forward_list — L'insertion et la suppression n'invalident jamais les itérateurs, références ou pointeurs vers les autres éléments. Seul l'itérateur de l'élément supprimé est invalidé.
array — Les itérateurs ne sont jamais invalidés (le tableau ne peut pas changer de taille).
Un bug courant est de modifier un vector dans une boucle range-based for. Si un push_back déclenche une réallocation, les itérateurs cachés de la boucle deviennent des références pendantes.
Si vous connaissez (ou pouvez estimer) le nombre d'éléments qu'un vector contiendra, appelez reserve() avant de le remplir. Cela préalloue la mémoire et évite des réallocations coûteuses :
- reserve(n) change la capacité mais pas la taille — aucun élément n'est ajouté.
- resize(n) change la taille — il construit effectivement des éléments par défaut.
- shrink_to_fit() est une demande non contraignante pour réduire la capacité à la taille actuelle.
Pour les boucles critiques qui construisent des vectors, un seul appel à reserve() peut réduire considérablement le surcoût d'allocation. Mesurez avec de grands ensembles de données pour voir la différence.
std::vectorest le choix par défaut — la mémoire contiguë signifie une itération optimisée pour le cache et un accès aléatoire en O(1)- Utilisez
reserve()pour préallouer la mémoire du vector lorsque la taille approximative est connue std::dequeoffre un push/pop en O(1) aux deux extrémités, mais n'est pas contiguëstd::listoffre une insertion/suppression en O(1) avec un itérateur et n'invalide jamais les autres itérateursstd::arrayest un wrapper sans surcoût autour des tableaux C avec une compatibilité STL complète- Les règles d'invalidation des itérateurs diffèrent selon le conteneur — connaissez-les pour éviter les bugs d'itérateurs pendants
Quiz — Testez vos connaissances
(15 XP)1. Quel conteneur séquentiel offre un `push_back` amorti en O(1) et une disposition mémoire contiguë ?
2. Que se passe-t-il pour tous les itérateurs d'un vector lorsqu'un `push_back` déclenche une réallocation ?
3. Quelle est la différence entre `reserve(n)` et `resize(n)` sur un vector ?