Learn C++ STL

Lesson 4 of 9 · vector: The Container You Reach for First

Module 2 · vector: The Container You Reach for First

When to Use vector and When Not To

FreeReading

In this lesson

  • Choose between vector and 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 a span view.

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.

QuestionAnswer that points to vectorAnswer that points elsewhere
1. Is it a sequence, or do you look things up by key?a sequence in some order: marks, readings, movesby 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 ordersearching 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 endat 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 moveda 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.

vector against array, deque, list and set Operation vector array deque list set insert at the end amortised O(1) not possible O(1) O(1) no positions insert at the front O(n) not possible O(1) O(1) no positions insert in the middle O(n) not possible O(n) O(1) at an iterator no positions insert keeping order O(n) not possible O(n) O(n) O(log n) find by value O(n) O(n) O(n) O(n) O(log n) index v[i] O(1) O(1) O(1) none none bytes per int (GCC 12) 4 + spare 4 about 4.2 24 40 iterators invalidated by an insert all, if it reallocates; else from it on never resized all iterators none none

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.

Which container: the decision flowchart 1. Do you look things up by key or value? yes set or map M8 to M10 no 2. Is the size fixed when you write the code? yes array M4 no 3. Where do you add and remove, most of the time? front deque M4 middle, at a place list M5 at the end 4. Are the elements big, and moved a lot? no vector yes a vector, plus a vector of indexes into it When in doubt: vector. Then measure before you switch.

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.

ParameterThe function mayCost of the call
const vector<int>& vread it, not change itO(1): a second name, no copy
vector<int>& vchange the caller's vectorO(1)
vector<int> vchange its own copy; the caller sees nothingO(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.

Run in Compiler
Example 1: Maria's leaderboard, in the right container

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.

Run in Compiler
Example 2: a queue at the counter, front removals

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.

Run in Compiler
Example 3: a vector is still right for most jobs

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.

Run in Compiler

Where this is used

  • The C++ Core Guidelines. Rule SL.con.2 says to prefer vector by 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::list is 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.

Brain teaser

Kenji stores "has this student handed in?" for 100,000 students, so he picks vector<bool>. Then he wants a second name for the first entry.

#include <vector>
using namespace std;

int main() {
    vector<bool> handed_in(100000);
    bool& first = handed_in[0];
    first = true;
    return 0;
}

The same two lines compile with vector<int> and int&. With bool, GCC 12 refuses at every command line: error: cannot bind non-const lvalue reference of type 'bool&' to an rvalue of type 'bool'. So what does handed_in[0] return, if not a bool&? And how many bytes might 100,000 of them take?

A bool needs only one bit of information. What would a library writer be tempted to do with the other seven bits of each byte?

Exercise 1Easy

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.

Run in Compiler
Exercise 2Medium

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.

Run in Compiler
Exercise 3Hard

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 Compiler

Common 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 array just 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 a span to 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

When to Use vector and When Not To | Learn C++ STL | Progsity