Module 0 ¡ What the STL Is, and Why It Changes How You Write C++
The Containers at a Glance: What Each One Is Good At
In this lesson
- List the containers this track teaches, in their three families.
- Name the one question each container answers fastest.
- Read a cost table written in O(1), O(log n) and O(n), without yet knowing how any container works inside.
Kenji has read Lesson 1 and has one question: "Which container is the fastest?" Amara, who writes the cleanest code on the team, answers with a question of her own: "Fastest at what?"
A container that adds to the end in one step may need a million steps to find a value. A container that finds a value in twenty steps cannot tell you quickly what is at position 5. This lesson is Amara's answer: every container, the question it is built for, and one table of costs.
You will not learn how any of them works inside yet. That is what Modules 2 to 10 are for. Here you get the whole shelf at once, so each later module fills in a picture you already hold.
Three families of containers
The containers come in three families, sorted by how they arrange their elements.
- Sequence containers keep elements in the order you put them in:
vector,string,array,dequeandlist. - Container adaptors wrap a sequence container and let you touch only one or two ends:
stack,queueandpriority_queue. - Associative containers arrange elements by their value, so you can find one quickly. They are
setandmap, theirmultiforms that allow repeats, and theirunorderedforms that use hashing instead of order.
To make the differences easy to see, every example below puts the same three numbers in, in the same order: 30, then 10, then 20. Watch what order they come back out in. That order alone tells you a lot about each container.
Declaring any container
std::vector<int> marks;
std::map<std::string, int> price;
std::vector,std::map: the container's name, from its own header (<vector>,<map>).<int>: the type of element it holds, written in angle brackets.<std::string, int>: a map holds pairs, so it needs two types, the key and the value.marks,price: the name you choose. A new container starts empty.
Sequence containers: in the order you put them
These five keep your elements in a row. They differ in where adding is cheap and whether the size can change.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
v.push_back(30);
v.push_back(10);
v.push_back(20);
std::cout << "vector:";
for (int x : v) std::cout << ' ' << x;
std::cout << " | v[1] = " << v[1] << '\n';
}
vector: 30 10 20 | v[1] = 10
Its question: "what is at position i?", answered in one step, plus cheap adding at the end. It is the default container, and the one to reach for first.
Run in Compiler#include <iostream>
#include <string>
int main()
{
std::string s = "cat";
s += "fish";
s.push_back('!');
std::cout << "string: " << s << " | size " << s.size() << " | s[0] = " << s[0] << '\n';
}
string: catfish! | size 8 | s[0] = c
Its question: the vector's, for text. It also knows text jobs such as joining with += and searching for a word. No '\0' to manage, and no buffer that can overflow.
#include <array>
#include <iostream>
int main()
{
std::array<int, 3> a = {30, 10, 20};
std::cout << "array:";
for (int x : a) std::cout << ' ' << x;
std::cout << " | size is always " << a.size() << '\n';
}
array: 30 10 20 | size is always 3
Its question: position i, for a count you know when you write the program. It is a C array that knows its own size, with no growing at all.
Run in Compiler#include <deque>
#include <iostream>
int main()
{
std::deque<int> d;
d.push_back(30);
d.push_front(10);
d.push_back(20);
std::cout << "deque:";
for (int x : d) std::cout << ' ' << x;
std::cout << " | d[0] = " << d[0] << '\n';
}
deque: 10 30 20 | d[0] = 10
Its question: "add or remove at either end", in one step each, and still position i. A deque (say "deck", a double-ended queue) is what a vector would be if adding at the front were cheap too.
Run in Compiler#include <iostream>
#include <list>
int main()
{
std::list<int> l = {30, 20};
auto it = l.begin();
++it;
l.insert(it, 10);
std::cout << "list:";
for (int x : l) std::cout << ' ' << x;
std::cout << '\n';
}
list: 30 10 20
Its question: "insert or remove right here", where you already stand, without moving any other element. It cannot answer "what is at position i?" quickly: it has no l[1] at all. It is the linked list you wrote by hand in C.
Adaptors: one door in, one door out
An adaptor takes a sequence container and hides most of it. You can only add and remove at the ends it allows. That is not a weakness: when your problem only ever needs the newest or the oldest item, the adaptor makes every other mistake impossible.
#include <iostream>
#include <stack>
int main()
{
std::stack<int> s;
s.push(30);
s.push(10);
s.push(20);
std::cout << "stack pops:";
while (!s.empty()) {
std::cout << ' ' << s.top();
s.pop();
}
std::cout << '\n';
}
stack pops: 20 10 30
Its question: "what was added last?". Like a pile of plates, the last one on is the first one off. Undo buttons and the C call stack you met in the C track work this way.
Run in Compiler#include <iostream>
#include <queue>
int main()
{
std::queue<int> q;
q.push(30);
q.push(10);
q.push(20);
std::cout << "queue pops:";
while (!q.empty()) {
std::cout << ' ' << q.front();
q.pop();
}
std::cout << '\n';
}
queue pops: 30 10 20
Its question: "what was added first?". It is a line at a shop counter: whoever arrived first is served first.
Run in Compiler#include <iostream>
#include <queue>
int main()
{
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(20);
std::cout << "priority_queue pops:";
while (!pq.empty()) {
std::cout << ' ' << pq.top();
pq.pop();
}
std::cout << '\n';
}
priority_queue pops: 30 20 10
Its question: "what is the biggest right now?", even while new elements keep arriving. It is an emergency room: the most urgent case goes next, whoever came first.
Run in CompilerAssociative containers: found by value
These do not keep your order at all. They arrange elements so that "is this value here?" is fast. The ordered ones (set, map) keep elements sorted; the unordered ones give up the order to be faster still on average.
#include <iostream>
#include <set>
int main()
{
std::set<int> s = {30, 10, 20, 10};
std::multiset<int> m = {30, 10, 20, 10};
std::cout << "set:";
for (int x : s) std::cout << ' ' << x;
std::cout << " | multiset:";
for (int x : m) std::cout << ' ' << x;
std::cout << " | is 20 in the set? " << s.count(20) << '\n';
}
set: 10 20 30 | multiset: 10 10 20 30 | is 20 in the set? 1
Its question: "is x here?", fast, with everything kept sorted. A set keeps one copy of each value, so the second 10 was dropped. A multiset keeps repeats.
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<std::string, int> price;
price["tea"] = 30;
price["bun"] = 10;
price["egg"] = 20;
std::cout << "map:";
for (const auto& p : price) std::cout << ' ' << p.first << '=' << p.second;
std::cout << " | egg costs " << price["egg"] << '\n';
}
map: bun=10 egg=20 tea=30 | egg costs 20
Its question: "what goes with this key?". A map is a set of keys, each carrying a value, kept sorted by key: bun comes out first because it is first alphabetically. A multimap allows one key to appear more than once.
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
int main()
{
std::unordered_set<int> seen = {30, 10, 20};
std::unordered_map<std::string, int> stock = {{"tea", 30}, {"bun", 10}};
std::cout << "unordered_set:";
for (int x : seen) std::cout << ' ' << x;
std::cout << " | is 10 seen? " << seen.count(10);
std::cout << " | buns in stock: " << stock["bun"] << '\n';
}
unordered_set: 20 10 30 | is 10 seen? 1 | buns in stock: 10
Its question: the set's and the map's, faster on average, when you never need the elements in order. The order they print in comes from hashing, not from you or from sorting. Another compiler's library may print a different order, so never rely on it.
Run in CompilerThe cost table
Costs in this track are written in big-O notation, where n is the number of elements in the container. You only need three of them for now, explained informally.
O(1), constant: the cost does not grow with n. One step with ten elements, one step with ten million.O(log n), logarithmic: the cost grows slowly. Doubling n adds about one step, so a million elements need about twenty.O(n), linear: the cost grows with n. Ten times the elements, ten times the work.
Now the table. Read a row as "this container, asked this question, costs this much". A dash means the container does not offer that operation at all. In the last three rows, "add" has no end or front. An element goes where its value says, so the add cost sits in the first column. Amortised means "on average over many calls"; Lesson 5 explains it.
| Container | Add at end | Add at front | Add in middle | Find a value | Element i | Walk in sorted order |
|---|---|---|---|---|---|---|
vector, string | O(1) amortised | O(n) | O(n) | O(n) | O(1) | no |
array | - | - | - | O(n) | O(1) | no |
deque | O(1) | O(1) | O(n) | O(n) | O(1) | no |
list | O(1) | O(1) | O(1) where you stand | O(n) | O(n) | no |
stack | O(1), top only | - | - | - | - | no |
queue | O(1) | - | - | - | - | no |
priority_queue | O(log n), to where it belongs | - | - | the biggest only, O(1) | - | no |
set, map and multi forms | O(log n), into sorted place | - | - | O(log n) | - | yes, O(n) for all |
unordered_set, unordered_map | O(1) on average | - | - | O(1) on average | - | no |
Two lines in the table deserve a second look. The list adds in the middle in O(1), but only once you are standing there; walking to the spot is O(n). And "on average" for the unordered containers is a real condition: on bad data their O(1) becomes O(n). Module 10 shows you that data.
So "which is fastest?" has no answer, and "fastest at finding by key, in sorted order?" has one: map. That is Amara's whole point.
Where this is used
- Linux's CFS scheduler. From kernel 2.6.23 to 6.5, the scheduler kept runnable tasks in a red-black tree ordered by how much CPU time each had used. That is the balanced tree GCC's library builds
std::setandstd::mapon. - Redis sorted sets. The usual way to build a game leaderboard. They keep members ordered by score and also find one member by name fast. Those are the two questions a
mapanswers, and Redis uses a skip list and a hash table for them. - Hunspell. The spell checker used by LibreOffice and Firefox loads its dictionary into a hash table. "Is this word in the dictionary?" is the
unordered_setquestion, asked once per word you type. - Emacs and VS Code. A text editor inserts in the middle of its text constantly, the one thing a plain
vectoris slow at. Emacs keeps a gap buffer and VS Code a piece tree, both built to make that insert cheap.
Common mistakes
1. Indexing a container that has no positions.
std::stack<int> s;
s.push(1);
std::cout << s[0] << "\n";
GCC 12 says error: no match for 'operator[]' (operand types are 'std::stack<int>' and 'int'). A stack only shows its top, with s.top(). The same message appears for a list, a set and a queue. If you need position i, you need a vector, deque or array.
2. Expecting an unordered container to come out sorted.
Example 11 put in 30, 10, 20 and printed 20 10 30. That order is neither yours nor sorted, and it can change when the container grows. If your output must be in order, use set or map, or copy the elements into a vector and sort it.
3. Choosing by speed alone.
Kenji builds a leaderboard with unordered_map because it is "the fastest". Then he must print the top ten, in order, after every game. The fast lookups win him nothing, and he sorts the whole board each time. Start from the question, as the table does, and the container picks itself.
Look at the outputs of Examples 1, 6, 7, 8 and 9. All five containers were given 30, 10, 20 in that order. For each one, write one sentence on why the numbers came out in the order they did.
Check yourself. You should have five different reasons: the order you added, newest first, oldest first, biggest first, and sorted.
Open Example 10 in the Playground. Add a fourth item, price["apple"] = 50;, after the egg line. Before you run it, write down the line you expect.
Rules. Then change std::map to std::unordered_map and add #include <unordered_map>. Run it again and compare the order.
Check yourself. With the map, apple=50 comes first. With the unordered map the order is whatever the hashing gives, and it is not alphabetical.
A web browser has a Back button and a Forward button. Visiting a new page clears the Forward history. Choose the container, or containers, you would use, and explain your choice in one paragraph using the table.
Rules. Say what happens to each container on Back, on Forward and on visiting a new page.
Check yourself. One good answer is two stacks: Back pops from one and pushes onto the other, and a new page clears the Forward stack. A deque with a position also works. Either way, every operation is O(1).
Common doubts
Do I have to memorise this table?
No. By the end of Module 10 you will know each row because you understand how each container is built. For now, keep the page open and use it to choose.
Why are stack and queue called adaptors and not containers?
Because they hold nothing themselves. A
stackis adequeunderneath, by default, with every operation except the top hidden away. Module 6 shows you how to choose what is underneath.If unordered_map is O(1) and map is O(log n), why ever use map?
Because a map keeps its keys sorted, and some jobs need that order. Also, O(log n) for a million keys is about twenty steps, which is very fast already. And an unordered map's O(1) is only an average, as the table says.
Is std::string really a container?
It behaves like one. It holds characters in a row, has
begin()andend(), and every algorithm works on it. It was designed separately and joined the STL family later, which is why it has extra text functions a vector does not.
Key takeaways
- Sequence containers keep your order; adaptors allow only one or two ends; associative containers arrange by value.
- Each container is built to answer one question fast, and no container is fastest at everything.
- O(1) does not grow with n, O(log n) grows slowly, O(n) grows with n.
- Ordered containers (
set,map) keep elements sorted;unorderedones are faster on average and keep no order you can rely on. - Choose the question first, then the container.
Next you will see the thread that ties all these containers to the algorithms: the iterator, moving.
End of lesson 3
Mark it done, and your progress moves with you.
Next: Iterators and Algorithms: How the Pieces Connect