Module 2 · vector: The Container You Reach for First
When to Use vector and When Not To
In this lesson
- Choose between
vectorand its neighbours by asking four questions about your data. - Read the comparison chart and the decision flowchart, and explain why the default is
vector. - Pass a vector to a function by
const&or&on purpose, and, in C++20, as aspanview.
Maria keeps a leaderboard sorted at all times. Every new score goes into the middle of a vector, at its place, so the list never needs sorting. It works, and with 200,000 scores it takes more than a second. Nothing is wrong with her code. She picked the wrong container. This lesson gives you the questions that would have told her which one to pick.
Four questions about your data
A container is a promise about which operations are cheap. So you choose one by asking what your program does most. Four questions decide almost every case.
| Question | Answer that points to vector | Answer that points elsewhere |
|---|---|---|
| 1. Is it a sequence, or do you look things up by key? | a sequence in some order: marks, readings, moves | by key or value ("is 42 here?", "Zara's score"): set or map, Modules 8 and 9 |
| 2. Do you read it by position, or search through it? | by position, v[i], or walking it in order | searching for a value again and again: a sorted container or a hash table, Modules 8 to 10 |
| 3. Where does it grow and shrink? | at the end | at the front too: deque, Module 4; in the middle, at a place you already hold: list, Module 5 |
| 4. How big is it, and how big is one element? | any size; elements small or rarely moved | a size fixed when you write the program: array, Module 4; huge elements moved often: a vector of indexes |
Maria's answers: a sequence, yes, but she searches it by value and inserts in the middle. Two of four point away from vector, towards a sorted container. So the questions name a container before you write any code.
The comparison chart
The chart puts vector beside the four containers it is most often confused with. The costs come from each container's page on cppreference. The memory row was measured: on GCC 12, a million ints, counting every byte each container asked for, one run on Compiler Explorer.
Read the vector column top to bottom: cheap at the end, cheap by index, cheapest in memory. Its weak rows are the front, the middle and search. Every other column wins one of those and loses something else. A list stores two pointers beside every int, so it needs six times the memory. A set keeps its values in order and finds one in O(log n), at ten times the memory, and with no index at all.
The last row matters as much as the costs. An iterator, a pointer or a reference to a vector element is valid only until the vector reallocates. After that it points into the freed old block. Even without a reallocation, an insert or erase in the middle shifts the elements after it, so their iterators go stale too. A list or a set never moves an element once it is stored. So a vector is the right choice when nobody holds on to its elements across a growth.
The decision flowchart
The same four ideas as a flowchart, in the order to ask them. Start at the top and follow your answers. Every exit names a container and the module that teaches it.
Maria's walk: question 1, she looks up scores by value to find their place, so yes. The arrow says set, or multiset when two players may tie, Module 8. So the flowchart sends her away from vector at the very first box.
Here is what the switch is worth, measured. The program inserts the same 200,000 pseudo-random numbers into a sorted vector, each at its place, and into a multiset. lower_bound finds the place in a sorted range; Module 13 teaches it, and here it only finds where to insert.
#include <algorithm>
#include <chrono>
#include <iostream>
#include <set>
#include <vector>
using namespace std;
int main() {
const int n = 200000;
vector<int> input(n);
unsigned x = 12345;
for (int& v : input) {
x = x * 1103515245u + 12345u;
v = (int)(x >> 1);
}
auto t0 = chrono::steady_clock::now();
vector<int> sorted_v;
for (int v : input) {
sorted_v.insert(lower_bound(sorted_v.begin(), sorted_v.end(), v), v);
}
auto t1 = chrono::steady_clock::now();
multiset<int> s;
for (int v : input) {
s.insert(v);
}
auto t2 = chrono::steady_clock::now();
chrono::duration<double, milli> a = t1 - t0, b = t2 - t1;
cout << "sorted vector: " << a.count() << " ms\n";
cout << "multiset: " << b.count() << " ms\n";
cout << (equal(sorted_v.begin(), sorted_v.end(), s.begin()) ? "same order\n" : "different\n");
return 0;
}
sorted vector: 1217.13 ms
multiset: 44.6139 ms
same order
One run on Compiler Explorer, GCC 12 at -O2 -std=c++17. Both hold the same numbers in the same order. Finding the place is fast in both; the vector's cost is shifting on average half its elements on every insert. So the right container was about 27 times faster here, with simpler code.
The honest default: vector, then measure
The chart can make a list look attractive: O(1) inserts anywhere. But a list puts each element in its own small block, wherever the allocator finds room. Walking it means jumping from block to block. A vector's elements sit side by side, and the processor reads memory in chunks, so the next element is usually already there.
#include <chrono>
#include <iostream>
#include <list>
#include <vector>
using namespace std;
int main() {
const int n = 1000000;
vector<int> v;
list<int> l;
for (int i = 0; i < n; i++) {
v.push_back(i % 100);
l.push_back(i % 100);
}
auto t0 = chrono::steady_clock::now();
long long sv = 0;
for (int x : v) sv += x;
auto t1 = chrono::steady_clock::now();
long long sl = 0;
for (int x : l) sl += x;
auto t2 = chrono::steady_clock::now();
chrono::duration<double, milli> dv = t1 - t0, dl = t2 - t1;
cout << "vector: sum " << sv << " in " << dv.count() << " ms\n";
cout << "list: sum " << sl << " in " << dl.count() << " ms\n";
return 0;
}
vector: sum 49500000 in 0.736809 ms
list: sum 49500000 in 4.59888 ms
Same million numbers, same loop, one run on Compiler Explorer at the Playground's flags: the list was about six times slower to walk. Module 17 measures this effect, the cache, properly. The C++ Core Guidelines give the rule this track follows. Use a vector by default, and switch only for a reason you can name or measure.
Big elements: keep them still, move indexes
Question 4 is about size. Moving an int copies 4 bytes. Moving a record with a long description copies all of it. If big records must be reordered or edited often, keep them in one vector that never changes, and move small int indexes in a second vector.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
vector<string> books{
"The C Programming Language, a long description...",
"Structure and Interpretation of Computer Programs, a long description...",
"Introduction to Algorithms, a long description...",
};
vector<int> reading_list{2, 0};
reading_list.insert(reading_list.begin(), 1);
for (int id : reading_list) {
cout << id << ": " << books[id].substr(0, 20) << '\n';
}
return 0;
}
1: Structure and Interp
2: Introduction to Algo
0: The C Programming La
The insert at the front moved two ints, not two long strings. The books stay where they are, so an index into books never goes stale. So big data stays put, and its order lives in a vector of indexes.
Passing a vector to a function
How you pass a vector says what the function may do with it. There are three choices, and each one is a promise.
| Parameter | The function may | Cost of the call |
|---|---|---|
const vector<int>& v | read it, not change it | O(1): a second name, no copy |
vector<int>& v | change the caller's vector | O(1) |
vector<int> v | change its own copy; the caller sees nothing | O(n): every element is copied |
#include <iostream>
#include <vector>
using namespace std;
int highest(const vector<int>& marks) {
int best = marks[0];
for (int m : marks) {
if (m > best) {
best = m;
}
}
return best;
}
void add_bonus(vector<int>& marks, int bonus) {
for (int& m : marks) {
m += bonus;
}
}
vector<int> passed(const vector<int>& marks) {
vector<int> result;
for (int m : marks) {
if (m >= 50) {
result.push_back(m);
}
}
return result;
}
int main() {
vector<int> marks{70, 45, 62, 38};
add_bonus(marks, 5);
cout << "highest " << highest(marks) << '\n';
cout << "passed:";
for (int m : passed(marks)) {
cout << ' ' << m;
}
cout << '\n';
return 0;
}
highest 75
passed: 75 50 67
Returning a vector by value, as passed does, is cheap: the compiler moves the result out instead of copying it, which lesson 05 explains. So take by const& to read, by & to change, and return a new vector by value.
In C++20
std::span<const int>, from <span>, is a view: a pointer and a length, no copy, no ownership. A function that takes one accepts a vector, a C array or a piece of either, so one function serves them all.
#include <iostream>
#include <span>
#include <vector>
using namespace std;
int total(span<const int> values) {
int sum = 0;
for (int x : values) {
sum += x;
}
return sum;
}
int main() {
vector<int> v{3, 1, 4, 1, 5};
int a[3] = {9, 2, 6};
cout << total(v) << ' ' << total(a) << '\n';
cout << total(span<const int>(v).subspan(1, 3)) << '\n';
return 0;
}
14 17
6
subspan(1, 3) is the three elements starting at index 1: 1, 4 and 1. A span does not own its elements, so it must not outlive the vector it views. The Run button opens the Playground on C++20.
The flowchart sent Maria to a sorted container. Here is her leaderboard with a multiset, as a preview of Module 8. Every insert lands in order, and printing walks it from the lowest score.
#include <iostream>
#include <set>
using namespace std;
int main() {
multiset<int> board;
int scores[] = {310, 120, 455, 120, 280};
for (int s : scores) {
board.insert(s);
}
cout << "board:";
for (int s : board) {
cout << ' ' << s;
}
cout << "\nlowest " << *board.begin() << ", highest " << *board.rbegin() << '\n';
return 0;
}
board: 120 120 280 310 455
lowest 120, highest 455
No index here: board[2] would not compile, because a set has no positions. That is the trade the chart shows.
Alice's shop serves customers from the front of a line, and new ones join at the back. The flowchart's question 3 answers "front", so a deque, Module 4. Its pop_front is O(1), where a vector's erase(v.begin()) shifts everyone.
#include <deque>
#include <iostream>
#include <string>
using namespace std;
int main() {
deque<string> line{"Bob", "Zara"};
line.push_back("Kenji");
while (!line.empty()) {
cout << "serving " << line.front() << ", " << line.size() - 1 << " waiting\n";
line.pop_front();
}
return 0;
}
serving Bob, 2 waiting
serving Zara, 1 waiting
serving Kenji, 0 waiting
line.size() - 1 is safe here, because the loop only runs while the line is not empty.
Zara logs the temperature every hour and then asks for the coldest and the average. The data is a sequence, it grows at the end, and it is walked in order: all four answers say vector.
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
vector<int> temps;
int t;
while (cin >> t) {
temps.push_back(t);
}
if (temps.empty()) {
cout << "no readings\n";
return 0;
}
int coldest = temps[0];
long long total = 0;
for (int x : temps) {
if (x < coldest) {
coldest = x;
}
total += x;
}
cout << temps.size() << " readings, coldest " << coldest
<< ", average " << (double)total / temps.size() << '\n';
return 0;
}
6 readings, coldest -3, average 2.5
That output is for the input 4 1 -3 0 6 7. Nothing here inserts at the front or searches by value, so nothing beats the vector.
Where this is used
- The C++ Core Guidelines. Rule SL.con.2 says to prefer
vectorby default unless you have a reason to use a different container. The reason it gives is the one above: a vector is compact, contiguous and fast to walk. - Bjarne Stroustrup's 2012 GoingNative keynote. The creator of C++ showed a vector beating a linked list at inserting into a sorted sequence, even though the list's insert is
O(1). Finding the place walks the list, and that walk is slow. - LLVM's Programmer's Manual. Its guide to picking a container steers readers towards vector-like containers, and says
std::listis rarely the right choice.
Common mistakes
1. Passing by value when you meant to change the vector.
void add_bonus(vector<int> marks) {
for (int& m : marks) {
m += 5;
}
}
No message at any command line. With {70, 85, 62}, the caller still printed 70 85 62 after the call. The function changed its own copy, and paid O(n) to make it. Write vector<int>& marks. You will forget the & because in C an array parameter was never a copy.
2. Keeping a pointer to an element, then growing the vector.
vector<int> marks{70, 85, 62};
int* first = &marks[0];
marks.push_back(91);
cout << "first mark: " << *first << '\n';
No message at any command line, and Success on the Playground, but the output was wrong and different each time: two runs printed first mark: 1540258676 and first mark: 1687701485. The capacity was 3, so push_back moved the marks to a new block and freed the old one. first still points into the freed block. Keep the index 0 instead, and read marks[0] when you need it. You will keep the pointer because it was valid when you took it.
3. Choosing a list and then indexing it.
list<int> scores{70, 85, 62};
cout << scores[1] << '\n';
An error at every command line: error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int'). A list has no index, as the chart's "index" row says. If you need positions, you needed a vector or a deque. You will try it because every container you met before had [].
4. Returning a reference to a local vector.
const vector<int>& make_marks() {
vector<int> marks{70, 85, 62};
return marks;
}
GCC 12 warns even on the Playground, without -Wall: warning: reference to local variable 'marks' returned [-Wreturn-local-addr]. Run anyway, a caller that read m.size() ended with Runtime error. The vector died at the function's closing brace. Return vector<int> by value; it is moved, not copied. You will write the & because "avoid the copy" was good advice for parameters.
Write int count_above(const vector<int>& v, int limit), which returns how many elements are greater than limit. Call it for each query.
Input. A line with n, a line with n integers, a line with q, then q limits, one per line.
Output. For each limit, one line with the count.
Constraints. 1 <= n, q <= 1000. Each value is between -1000000 and 1000000.
Sample. Input 5, 4 9 7 1 9, 2, 5, 9 gives 3 and 0.
#include <iostream>
#include <vector>
using namespace std;
// Return how many elements of v are greater than limit.
// The const& promises not to change v, and avoids a copy per call.
int count_above(const vector<int>& v, int limit) {
return 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> v(n);
for (int& x : v) {
cin >> x;
}
int q;
cin >> q;
for (int i = 0; i < q; i++) {
int limit;
cin >> limit;
cout << count_above(v, limit) << '\n';
}
return 0;
}
Not graded on its own. Try the parameter as vector<int> v too: the answers stay the same, and every call copies the whole vector.
David describes workloads in three words, and you answer with the flowchart's container. The words are: how it is read (position or key), its size (fixed or grows), and where it changes (end, front or middle).
Input. A line with q, then q lines of three words.
Output. For each line, one word: set if it is read by key, else array if its size is fixed, else deque for front, list for middle, vector for end.
Constraints. 1 <= q <= 100. The words are exactly as listed.
Sample. Input 3, key grows middle, position grows end, position grows front gives set, vector and deque.
#include <iostream>
#include <string>
using namespace std;
int main() {
int q;
cin >> q;
for (int i = 0; i < q; i++) {
string read, size, change;
cin >> read >> size >> change;
// Ask the flowchart's questions in its order, and print one word.
}
return 0;
}
Not graded on its own. The order of the questions is the point: key fixed front is still set.
Feel Maria's cost yourself. Read numbers until the input ends, and keep a vector sorted by inserting each one at its place. Print the index where each number landed, then the final list.
Input. Integers until the end of the input.
Output. One line with the landing index of each number, in input order, then one line with the sorted list. Equal numbers land before the ones already there.
Constraints. 1 to 100000 numbers, each between -1000000000 and 1000000000.
Sample. Input 5 2 8 2 gives 0 0 2 0 and 2 2 5 8.
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
vector<int> sorted_v;
vector<int> landed;
int x;
while (cin >> x) {
// lower_bound(sorted_v.begin(), sorted_v.end(), x) is the first place
// whose value is not less than x. Its distance from begin() is the index.
// Insert x there, and remember the index.
}
// Print the indexes on one line, then the sorted list on the next.
return 0;
}
Not graded on its own. Then time 100,000 random inputs on the Playground: each insert shifts about half the vector, the cost this lesson measured.
Run in CompilerCommon doubts
If a list inserts in
O(1), why is it so rarely chosen?Because the
O(1)needs an iterator already at the place. Finding the place is a walk,O(n), and a slow walk, measured six times slower than a vector's above. Lists win when you already hold the position, as Module 5 shows.Is an
arrayjust a worse vector?No.
std::array<int, 12>has a size fixed at compile time and no heap block at all. For twelve months or six die faces it is exactly right. Module 4 teaches it.Can a sorted vector ever beat a set?
Yes, when you fill it once, sort it once, and then only search it. Searching a sorted vector with binary search is
O(log n)too, and it is compact. Maria's problem was inserting into it again and again.Does returning a vector from a function copy it?
No. Since C++11 the result is moved, which hands over the block in
O(1), and in many cases the compiler builds it in place. Lesson 05 names both.
Key takeaways
- Ask four questions: sequence or key, position or search, where it grows, how big an element is.
- A vector wins at the end, by index and in memory; it loses at the front, in the middle and at search by value.
- A reallocation invalidates every pointer, reference and iterator into the vector; a list or a set never moves an element.
- When in doubt, use a vector and measure: a list walked six times slower, and a set inserted in order 27 times faster.
- Pass by
const&to read and&to change, return by value, and in C++20 take aspanto accept any contiguous range. - Go deeper: Under the Hood, how a vector grows and what that breaks (Pro).
Next, the Pro lesson 05 opens the vector up: how the capacity grows on our compiler, and what a reallocation breaks.
End of lesson 4
Mark it done, and your progress moves with you.
Next: Under the Hood: How a vector Grows, and What That Breaks