Contenitori sequenziali: vector, deque, list, array
Esplora i contenitori sequenziali della STL — vector, deque, list, forward_list e array. Scopri i compromessi prestazionali, i layout di memoria e le regole di invalidazione degli iteratori.
Cosa sono i contenitori sequenziali?
I contenitori sequenziali memorizzano gli elementi in un ordine lineare rigoroso. La STL fornisce cinque contenitori sequenziali, ciascuno ottimizzato per diversi pattern di accesso e modifica:
- std::vector — array dinamico, memoria contigua
- std::deque — coda a doppia estremità, inserimento veloce a entrambe le estremità
- std::list — lista doppiamente concatenata
- std::forward_list — lista concatenata singola
- std::array — array a dimensione fissa, allocato sullo stack
Scegliere il contenitore giusto è una delle decisioni più impattanti sulle prestazioni in C++. I fattori chiave sono il pattern di accesso, il pattern di inserimento/cancellazione e la località della cache.
std::vector — La scelta predefinita
std::vector è il contenitore più utilizzato. Memorizza gli elementi in un singolo blocco contiguo di memoria, garantendo eccellenti prestazioni della cache e accesso casuale O(1). L'aggiunta di elementi con push_back è ammortizzata O(1) perché il vector raddoppia la propria capacità quando è pieno.
#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 e forward_list
std::deque fornisce inserimento e cancellazione O(1) sia in testa che in coda, ma utilizza un layout di memoria segmentato (array di blocchi a dimensione fissa) anziché una singola allocazione contigua. std::list è una lista doppiamente concatenata che offre inserimento/cancellazione O(1) in qualsiasi punto dato un iteratore, a scapito di una scarsa località della cache. std::forward_list è una lista concatenata singola che utilizza meno memoria per nodo ma supporta solo lo scorrimento in avanti.
#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 — Dimensione fissa, zero overhead
std::array incapsula un array in stile C in una classe appropriata, fornendo .size(), .at(), iteratori e compatibilità con tutti gli algoritmi STL — con zero overhead rispetto a un T[N] grezzo.
#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);
}Scegliere il contenitore sequenziale giusto
Usa questa guida decisionale:
Usa std::vector (l'opzione predefinita) quando hai bisogno di accesso casuale, iterazione cache-friendly o push/pop in coda. La stragrande maggioranza del codice dovrebbe usare vector.
Usa std::deque quando hai bisogno di inserimento/rimozione frequente sia in testa che in coda (ad es., una finestra scorrevole, coda BFS).
Usa std::list quando hai bisogno di inserimento/rimozione O(1) nel mezzo dato un iteratore, o quando hai bisogno di splice() per spostare sotto-liste tra liste senza copiare.
Usa std::forward_list quando la memoria per nodo è importante e ti serve solo lo scorrimento in avanti.
Usa std::array quando la dimensione è nota a tempo di compilazione e non cambierà mai.
Nota sulle prestazioni: A causa degli effetti della cache della CPU, std::vector spesso supera std::list anche per inserimenti nel mezzo sull'hardware moderno, fino a dimensioni sorprendentemente grandi (migliaia di elementi). Misura sempre prima di scegliere una lista concatenata.
Ogni contenitore ha regole diverse su quando iteratori, puntatori e riferimenti diventano non validi:
vector — L'inserimento che provoca una riallocazione invalida tutti gli iteratori. L'inserimento senza riallocazione invalida gli iteratori al punto di inserimento o successivi. La cancellazione invalida gli iteratori al punto di cancellazione o successivi.
deque — L'inserimento in testa o in coda invalida tutti gli iteratori ma non i riferimenti/puntatori agli elementi esistenti. L'inserimento nel mezzo invalida tutto. La cancellazione in testa/coda invalida solo l'iteratore dell'elemento cancellato.
list / forward_list — Inserimento e cancellazione non invalidano mai iteratori, riferimenti o puntatori ad altri elementi. Solo l'iteratore dell'elemento cancellato viene invalidato.
array — Gli iteratori non vengono mai invalidati (l'array non può cambiare dimensione).
Un bug comune è modificare un vector all'interno di un ciclo range-based for. Se un push_back provoca una riallocazione, gli iteratori nascosti del ciclo diventano pendenti.
Se conosci (o puoi stimare) quanti elementi conterrà un vector, chiama reserve() prima di riempirlo. Questo pre-alloca la memoria ed evita riallocazioni costose:
- reserve(n) modifica la capacità ma non la dimensione — non vengono aggiunti elementi.
- resize(n) modifica la dimensione — costruisce effettivamente gli elementi con il costruttore predefinito.
- shrink_to_fit() è una richiesta non vincolante per ridurre la capacità alla dimensione attuale.
Per cicli critici che costruiscono vector, una singola chiamata a reserve() può ridurre drasticamente l'overhead delle allocazioni. Misura con grandi set di dati per vedere la differenza.
std::vectorè la scelta predefinita — la memoria contigua significa iterazione cache-friendly e accesso casuale O(1)- Usa
reserve()per pre-allocare la memoria del vector quando la dimensione approssimativa è nota std::dequefornisce push/pop O(1) sia in testa che in coda, ma non è contiguostd::listoffre insert/erase O(1) con un iteratore e non invalida mai gli altri iteratoristd::arrayè un wrapper a zero overhead attorno agli array C con piena compatibilità STL- Le regole di invalidazione degli iteratori differiscono per contenitore — conoscile per evitare bug da iteratori pendenti
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Quale contenitore sequenziale fornisce `push_back` ammortizzato O(1) e layout di memoria contigua?
2. Cosa succede a tutti gli iteratori del vector quando un `push_back` provoca una riallocazione?
3. Qual è la differenza tra `reserve(n)` e `resize(n)` su un vector?