Standard Template Library
Reviewed & published by Brayan K
By the end of this lesson you'll be able to pick the right STL container for any job — vector, string, map/unordered_map, set/unordered_set, pair — and insert, look up, and iterate over each one with iterators and range-for.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Store and grow lists with std::vector and read them by index
- Map keys to values with std::map and std::unordered_map
- Keep a collection of unique items with std::set / std::unordered_set
- Bundle two values together with std::pair
- Iterate any container with begin/end, range-for, and auto
- Choose ordered vs unordered containers by their cost trade-off
💡 Real-World Analogy
The STL is a professional toolbox. You don't whittle your own hammer — you reach for the right tool. A vector is a numbered shelf you can add to. A map is a dictionary: look up a word (the key) to get its definition (the value). A set is a guest list where each name appears once. A pair is a luggage tag tying a name to a number. Picking the wrong container is like using a wrench to hammer a nail — it works badly. This lesson is about choosing the right tool.
📦 The Core Containers
| Container | Holds | Analogy | Reach for it when… |
|---|---|---|---|
| vector<T> | Ordered list of T | Numbered shelf | You want a resizable array (the default choice) |
| string | Text | Sentence | You're working with characters/words |
| map<K,V> | Sorted key→value | Dictionary | You look things up by key AND want sorted keys |
| unordered_map<K,V> | Hashed key→value | Hash table | You want the fastest lookups, order doesn't matter |
| set<T> | Unique, sorted | Sorted guest list | No duplicates allowed and you want them sorted |
| pair<A,B> | Two values | Luggage tag | You need to return/keep two things as one |
1. std::vector — the resizable array
A vector is a list that grows and shrinks automatically, so it's the container you reach for 90% of the time. You add to the end with push_back, read any item by position with [] (indexes start at 0), and walk through it with a range-for. To insert or erase in the middle you give a position — an iterator like begin() + 1. Read this worked example, run it, then you'll write one.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
// A vector is a list that grows and shrinks for you.
// vector<T> holds many values of ONE type T.
vector<string> fruits = {"Apple", "Banana", "Cherry"};
// INSERT at the end — the cheap, normal way to add.
fruits.push_back("Date"); // {Apple, Banana, Cherry, Date}
// LOOKUP by position with [] — indexes start at 0.
cout << "First: " << fruits[0] << endl; // First: Apple
cout << "Size: " << fruits.size() << endl; // Size: 4
// ITERATE — a range-for reads each item in order.
// 'const string &f' = read-only reference (no copy made).
for (const string &f : fruits) {
cout << "- " << f << endl; // - Apple / - Banana / ...
}
// INSERT/ERASE in the middle use an ITERATOR (a position).
// begin() points at index 0; begin()+1 is the second slot.
fruits.insert(fruits.begin() + 1, "Avocado"); // after Apple
fruits.erase(fruits.end() - 1); // drop the last one
cout << "Now: ";
for (const string &f : fruits) cout << f << " ";
cout << endl; // Now: Apple Avocado Banana Cherry
return 0;
}
// ✅ Expected output:
// First: Apple
// Size: 4
// - Apple
// - Banana
// - Cherry
// - Date
// Now: Apple Avocado Banana CherryYour turn. The program below is almost complete — fill in the two blanks marked ___ using the hints, then run it.
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 1) Make a vector<int> called "scores" with 90, 85, 95
vector<int> scores = ___; // 👉 use { } braces, e.g. {90, 85, 95}
// 2) Add the score 70 to the END of the vector
scores.___(70); // 👉 the method that appends is push_back
// These lines already work once your code above is right:
int total = 0;
for (int s : scores) total += s;
cout << "Count: " << scores.size() << endl;
cout << "Total: " << total << endl;
// ✅ Expected output:
// Count: 4
// Total: 340
return 0;
}2. std::map & std::unordered_map — key → value
A map stores key → value pairs, like a dictionary where you look up a word to get its meaning. std::map<K,V> keeps keys sorted (lookups cost O(log n)); std::unordered_map has the same interface but hashes keys for O(1) average lookups with no order. The big gotcha: myMap[key] inserts a default value when the key is missing, so to merely check a key use .count() or .find() — they never insert.
#include <iostream>
#include <map>
#include <unordered_map>
#include <string>
using namespace std;
int main() {
// A map stores KEY -> VALUE pairs. Here: name (string) -> age (int).
// std::map keeps keys SORTED; lookups cost O(log n).
map<string, int> ages;
// INSERT — two ways:
ages["Alice"] = 25; // [] assigns (or creates) a key
ages.insert({"Bob", 30}); // insert() won't overwrite an existing key
// LOOKUP. WARNING: ages["Eve"] would CREATE Eve with value 0.
// To only check, use .count() or .find() — they never insert.
if (ages.count("Alice")) cout << "Alice: " << ages["Alice"] << endl; // 25
if (ages.count("Eve") == 0) cout << "Eve not found" << endl;
// .find() returns an iterator: == end() means "not present".
auto it = ages.find("Bob");
if (it != ages.end()) cout << it->first << " is " << it->second << endl; // Bob is 30
// ITERATE — std::map walks keys in SORTED order automatically.
// Structured binding [name, age] unpacks each pair (C++17).
for (const auto &[name, age] : ages) {
cout << name << " => " << age << endl; // Alice => 25 / Bob => 30
}
// unordered_map is the SAME interface but HASH-based: O(1) average
// lookups, but NO sorted order. Reach for it when speed > ordering.
unordered_map<string, int> stock;
stock["pens"] = 12;
cout << "Pens in stock: " << stock["pens"] << endl; // 12
return 0;
}
// ✅ Expected output:
// Alice: 25
// Eve not found
// Bob is 30
// Alice => 25
// Bob => 30
// Pens in stock: 12Now you try. Build a tiny phone book and check a key safely — fill in the two blanks:
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — a phone book: name -> extension number.
map<string, int> phonebook;
// 1) Add "Sam" with extension 101 using []
phonebook["Sam"] = ___; // 👉 the number 101
// 2) Check whether "Sam" exists WITHOUT creating a new key.
// Fill in the method that counts a key but never inserts.
if (phonebook.___("Sam")) { // 👉 use count
cout << "Sam is at extension " << phonebook["Sam"] << endl;
}
if (phonebook.count("Ghost") == 0) {
cout << "Ghost has no extension" << endl;
}
// ✅ Expected output:
// Sam is at extension 101
// Ghost has no extension
return 0;
}3. std::set, std::pair & iterators
A set holds unique values — try to add a duplicate and it's silently ignored — and std::set keeps them sorted (std::unordered_set is the hash-based, unordered twin). A pair glues two values into one object you reach with .first and .second. Underneath every container are iterators: begin() points at the first element, end() points one past the last (a stop sign, not a value), and *it reads what the iterator points at. auto spares you from spelling out the iterator's type.
#include <iostream>
#include <set>
#include <unordered_set>
#include <utility> // for std::pair
#include <string>
using namespace std;
int main() {
// === SET — a collection of UNIQUE values (no duplicates) ===
// std::set keeps them SORTED; duplicates are silently dropped.
set<int> seen = {5, 3, 8, 3, 5}; // stored as: 3 5 8
seen.insert(4); // INSERT -> 3 4 5 8
seen.insert(3); // already there -> ignored
cout << "Set: ";
for (int n : seen) cout << n << " "; // ITERATE in sorted order: 3 4 5 8
cout << endl;
// LOOKUP — .count() is 1 if present, 0 if not.
cout << "Has 4? " << seen.count(4) << endl; // Has 4? 1
cout << "Has 7? " << seen.count(7) << endl; // Has 7? 0
// unordered_set = same idea, hash-based: O(1) average, NOT sorted.
unordered_set<string> tags = {"news", "tech", "news"}; // 2 unique
cout << "Unique tags: " << tags.size() << endl; // 2
// === PAIR — glue two values together into one object ===
pair<string, int> player = {"Lara", 9001};
cout << player.first << " scored " << player.second << endl; // Lara scored 9001
// === ITERATORS with auto ===
// begin() = first element, end() = ONE PAST the last (a stop sign).
// 'auto' lets the compiler name the iterator type for you.
for (auto it = seen.begin(); it != seen.end(); ++it) {
cout << *it << " "; // *it reads the value the iterator points at
}
cout << endl; // 3 4 5 8
return 0;
}
// ✅ Expected output:
// Set: 3 4 5 8
// Has 4? 1
// Has 7? 0
// Unique tags: 2
// Lara scored 9001
// 3 4 5 8🔎 Deep Dive: ordered vs unordered cost
The map/set pair are built on balanced trees: every insert, erase, and lookup costs O(log n), and you get sorted order for free. The unordered_ pair use a hash table: O(1) average, but the elements come out in no useful order.
Rule of thumb: if you need the keys sorted (printing alphabetically, range queries), use the ordered version. If you just need fast membership tests or lookups and don't care about order, the unordered version is usually faster.
map<string,int> ordered, O(log n) -> sorted iteration
unordered_map<string,int> hashed, O(1) avg -> no order, faster
// same trade-off for set vs unordered_setPro Tips
- 💡 Default to vector: it's contiguous and cache-friendly. Only switch containers when you have a reason (unique items → set, key lookups → map).
- 💡 Structured bindings read maps cleanly: for (const auto &[k, v] : m) unpacks each pair (C++17).
- 💡 Counting with a map is a one-liner: counts[word]++; — a missing key starts at 0, so ++ makes it 1.
- 💡 auto for iterators: auto it = m.find(k); beats writing map<string,int>::iterator by hand.
Common Errors (and the fix)
- Iterator invalidation (crash / garbage): erasing while iterating leaves your iterator dangling. Use the value erase() returns: it = v.erase(it); and only ++it when you didn't erase.
- [] silently inserts into a map: if (m["Eve"]) ... just created Eve with value 0. To check a key, use m.count("Eve") or m.find("Eve") != m.end().
- Expecting unordered_map to be sorted: it isn't — it's hashed. If you print it and want order, use map (or copy into a vector and sort).
- .find() vs [] confusion: [] returns the value (and may insert); .find() returns an iterator. Compare .find() to .end() to test existence, then read it->second for the value.
- Dereferencing end(): end() is one past the last element — *v.end() is undefined behaviour. Always check it != v.end() first.
📋 Quick Reference — which container?
| You need… | Use | Insert | Lookup |
|---|---|---|---|
| An ordered, resizable list | vector<T> | v.push_back(x) | v[i] |
| Key → value, sorted keys | map<K,V> | m[k] = v | m.find(k) |
| Key → value, fastest | unordered_map<K,V> | m[k] = v | m.count(k) |
| Unique items, sorted | set<T> | s.insert(x) | s.count(x) |
| Unique items, fastest | unordered_set<T> | s.insert(x) | s.count(x) |
| Two values as one | pair<A,B> | {a, b} | p.first / p.second |
Mini-Challenge: Word Frequency Counter
No blanks this time — just a brief and an outline. Count how many times each word appears using a map<string,int>, then print the tallies. Because std::map sorts keys, your output comes out alphabetically for free. Build it, run it, and check against the expected output.
#include <iostream>
#include <map>
#include <string>
#include <sstream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: Word frequency counter
// 1. Start from this text:
string text = "red blue red green blue red";
//
// 2. Make a map<string, int> counts;
// 3. Read each word and do counts[word]++;
// (a brand-new key starts at 0, so ++ makes it 1 — perfect.)
// Tip: feed the text into an istringstream and use while (ss >> word)
// 4. Iterate the map and print "<word>: <count>"
//
// ✅ Expected output (map prints keys in sorted order):
// blue: 2
// green: 1
// red: 3
// your code here
return 0;
}🎉 Lesson Complete
- ✅ vector<T> is your default resizable list — push_back, index with [], range-for to iterate
- ✅ map/unordered_map store key → value; ordered (O(log n)) vs hashed (O(1) avg)
- ✅ set/unordered_set keep unique items; pair bundles two values
- ✅ [] on a map inserts missing keys — use .count()/.find() to just check
- ✅ Iterators: begin()/end(), *it reads, auto names the type, end() is one-past-last
Practice quiz
Which container is the default 'resizable list' you reach for most often?
- std::map
- std::set
- std::vector
- std::pair
Answer: std::vector. std::vector is a contiguous, cache-friendly resizable array — the everyday default container.
How do you add an element to the end of a std::vector?
- v.push_back(x)
- v.append(x)
- v.insert(x)
- v.add(x)
Answer: v.push_back(x). push_back appends to the end of a vector.
What does std::map store, and what order are its keys in?
- Unique values, no order
- Key to value pairs in insertion order
- A single value per index
- Key to value pairs, with keys kept sorted (O(log n))
Answer: Key to value pairs, with keys kept sorted (O(log n)). std::map stores key to value pairs in sorted key order using a balanced tree, costing O(log n) per operation.
What is the gotcha with using myMap[key] to look up a key?
- It is slower than .find()
- If the key is missing, [] inserts a default-constructed entry for it
- It throws an exception when the key is missing
- It returns an iterator instead of a value
Answer: If the key is missing, [] inserts a default-constructed entry for it. operator[] inserts a default value for a missing key, so to merely check use .count() or .find().
Which method checks whether a key exists in a map WITHOUT inserting it?
- map.count(key) or map.find(key)
- map[key]
- map.at(key) always
- map.exists(key)
Answer: map.count(key) or map.find(key). count() and find() never insert; [] would create the key. Use them to test existence.
What is the main difference between std::map and std::unordered_map?
- unordered_map cannot store strings
- map is faster for all operations
- map is a sorted tree (O(log n)); unordered_map is a hash table (O(1) average) with no order
- unordered_map keeps keys sorted
Answer: map is a sorted tree (O(log n)); unordered_map is a hash table (O(1) average) with no order. map keeps keys sorted at O(log n); unordered_map hashes keys for O(1) average lookups but no ordering.
What happens when you insert a duplicate value into a std::set?
- It is stored twice
- It is silently ignored — sets hold only unique values
- It throws an exception
- It overwrites the existing one
Answer: It is silently ignored — sets hold only unique values. A set holds unique values; adding a duplicate is silently dropped.
What does end() point to in an STL container?
- The last element
- The first element
- A null pointer
- One past the last element — a stop sign, never dereference it
Answer: One past the last element — a stop sign, never dereference it. begin() is the first element but end() is one past the last; dereferencing end() is undefined behavior.
How do you read the value an iterator it points to?
- it.value
- *it
- it->value()
- &it
Answer: *it. Dereference the iterator with *it to read the element it points at.
Why might counting words with counts[word]++; work even for a brand-new word?
- It throws and is caught
- It inserts the word twice
- A missing key is default-constructed to 0, so ++ makes it 1
- It only works after calling .insert()
Answer: A missing key is default-constructed to 0, so ++ makes it 1. operator[] on a missing key creates it with value 0, so ++ immediately bumps it to 1 — a clean tally one-liner.
Continue this course
- Previous: Templates
- Next: Memory Management — Allocate and free memory manually with new/delete and understand RAII
- Quick reference: C++ cheat sheet
- From the blog: C++ STL Containers: A Complete Guide
Frequently asked questions
When should I use map vs unordered_map?
Use unordered_map when you only need fast key lookups — it hashes keys for O(1) average access. Use map (a balanced tree, O(log n)) when you also need the keys to stay in sorted order, for example to print them alphabetically or to find ranges. If you don't care about order, unordered_map is usually faster.
What is the difference between .find() and [] on a map?
[] returns the value for a key AND inserts a default-constructed entry if the key is missing — so just reading a missing key silently grows your map. .find() returns an iterator and never inserts: compare it to .end() to test existence. Use [] to assign, .find() or .count() to check.
Why is set sorted but unordered_set is not?
std::set and std::map are built on balanced binary trees, which keep elements in sorted order as a side effect — that ordering is what costs the O(log n) per operation. The unordered_ versions use a hash table, which scatters elements into buckets for O(1) average speed but gives up any meaningful order.
What does end() point to — the last element?
No. begin() points at the first element, but end() points ONE PAST the last — think of it as a stop sign, not a real value. That is why loops run while it != container.end() and why a successful .find() returns something other than end(). Never dereference end().
What is iterator invalidation?
Adding to or erasing from a container can move its internal storage, leaving any iterators (and the range-for loop using them) pointing at freed memory — often a crash. The safe pattern is to use the iterator that erase() returns: it = v.erase(it); and only ++it when you did not erase. For vectors, push_back can also invalidate iterators if it reallocates.