3. The Standard Template Library
In Gorgo Starting C++ you learned std::vector and std::array — the two workhorses of sequential storage. But not every problem is a list. Sometimes you need to look up a value by key, enforce uniqueness, or process items in priority order. The Standard Template Library (STL) provides a rich set of containers, each optimized for different access patterns. In Chapter 2 you learned that these containers are class templates — they work with any type. In this chapter you will learn the associative containers, unordered containers, and container adaptors, and how to choose the right one for the job.
Associative Containers
Associative containers store elements in sorted order and provide efficient lookup, insertion, and deletion — typically O(log n). They are implemented as balanced binary search trees (usually red-black trees).
std::map
std::map<Key, Value> stores key-value pairs sorted by key. Each key is unique:
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<std::string, int> album_years;
album_years["Elephant"] = 2003;
album_years["Hot Fuss"] = 2004;
album_years["Funeral"] = 2004;
for (const auto& [album, year] : album_years) {
std::cout << album << ": " << year << "\n";
}
return 0;
}Elephant: 2003
Funeral: 2004
Hot Fuss: 2004The output is sorted alphabetically by key. The const auto& [album, year] syntax is a structured binding that unpacks each std::pair<const std::string, int> in the map.
Key operations:
// Insert or update
album_years["X&Y"] = 2005;
album_years.insert({"Absolution", 2003});
// Lookup
if (album_years.count("Funeral") > 0) {
std::cout << "Found\n";
}
// C++20: contains()
// bool contains(const Key& key) const;
if (album_years.contains("Funeral")) {
std::cout << "Found\n";
}
// Safe access with find()
// iterator find(const Key& key);
auto it = album_years.find("Elephant");
if (it != album_years.end()) {
std::cout << it->second << "\n"; // 2003
}
Trap: Using operator[] on a key that does not exist will insert a default-constructed value. If you just want to check whether a key exists, use find() or contains() instead.
std::set
std::set<T> stores unique values in sorted order. It is like a map with only keys:
#include <iostream>
#include <set>
#include <string>
int main()
{
std::set<std::string> genres;
genres.insert("Rock");
genres.insert("Pop");
genres.insert("Rock"); // duplicate — ignored
genres.insert("Indie");
for (const auto& g : genres) {
std::cout << g << "\n";
}
std::cout << "Size: " << genres.size() << "\n";
return 0;
}Indie
Pop
Rock
Size: 3insert() returns a std::pair<iterator, bool> — the bool tells you whether the insertion actually happened:
auto [it, inserted] = genres.insert("Pop");
if (!inserted) {
std::cout << "Pop was already in the set\n";
} std::multimap and std::multiset
std::multimap and std::multiset are like map and set but allow duplicate keys:
#include <iostream>
#include <map>
#include <string>
int main()
{
std::multimap<std::string, std::string> songs_by_genre;
songs_by_genre.insert({"Rock", "Seven Nation Army"});
songs_by_genre.insert({"Rock", "Mr. Brightside"});
songs_by_genre.insert({"Pop", "Crazy in Love"});
songs_by_genre.insert({"Pop", "Toxic"});
// equal_range returns a pair of iterators: [first match, past last match)
// pair<iterator, iterator> equal_range(const Key& key);
auto [begin, end] = songs_by_genre.equal_range("Rock");
std::cout << "Rock songs:\n";
for (auto it = begin; it != end; ++it) {
std::cout << " " << it->second << "\n";
}
return 0;
}Rock songs:
Seven Nation Army
Mr. Brightside
Tip: multimap does not have operator[] because a key can map to multiple values. Use find(), equal_range(), or a range-based loop to access elements.
Unordered Containers
Unordered containers use hash tables instead of trees. They provide O(1) average-case lookup, insertion, and deletion — faster than the O(log n) of ordered containers. The trade-off is that elements are not stored in any particular order.
std::unordered_map
std::unordered_map<Key, Value> is the hash-table equivalent of std::map:
#include <iostream>
#include <string>
#include <unordered_map>
int main()
{
std::unordered_map<std::string, int> play_counts;
play_counts["Rehab"] = 42;
play_counts["Poker Face"] = 87;
play_counts["Use Somebody"] = 55;
play_counts["Rehab"] += 1;
for (const auto& [song, count] : play_counts) {
std::cout << song << ": " << count << " plays\n";
}
return 0;
}The output order is not alphabetical — it depends on the hash function and internal bucket layout.
The API is nearly identical to std::map: operator[], find(), contains(), insert(), erase() all work the same way.
std::unordered_set
std::unordered_set<T> is the hash-table equivalent of std::set:
#include <iostream>
#include <string>
#include <unordered_set>
int main()
{
std::unordered_set<std::string> tags = {"chill", "summer", "dance", "chill"};
std::cout << "Tags (" << tags.size() << "):\n";
for (const auto& t : tags) {
std::cout << " " << t << "\n";
}
return 0;
}Duplicates are ignored, just like std::set, but the elements are not sorted.
Wut: To use a custom type as a key in an unordered container, you must provide a hash function and an equality operator. Standard types like std::string, int, and double already have hash functions.
Ordered vs. Unordered
| Feature | map/set | unordered_map/unordered_set |
|---|---|---|
| Order | Sorted by key | No guaranteed order |
| Lookup | O(log n) | O(1) average, O(n) worst |
| Insertion | O(log n) | O(1) average, O(n) worst |
| Requires | operator< or comparator | Hash function + operator== |
| Memory | Tree nodes (pointer overhead) | Hash buckets (may waste space) |
Use ordered containers when you need sorted iteration or range queries. Use unordered containers when you only need fast lookup by key.
Container Adaptors
Container adaptors wrap an underlying container and restrict its interface to provide a specific behavior. They are not full containers — you cannot iterate through them.
std::stack
std::stack<T> provides LIFO (last in, first out) access:
#include <iostream>
#include <stack>
#include <string>
int main()
{
std::stack<std::string> history;
history.push("Chasing Cars");
history.push("How to Save a Life");
history.push("Fix You");
while (!history.empty()) {
std::cout << history.top() << "\n";
history.pop();
}
return 0;
}Fix You
How to Save a Life
Chasing CarsKey methods:
void push(const T& value); // add to top
void pop(); // remove from top (does not return it)
T& top(); // access top element
bool empty() const;
size_t size() const;
Trap: pop() does not return the removed element. You must call top() first to get the value, then pop() to remove it.
std::queue
std::queue<T> provides FIFO (first in, first out) access:
#include <iostream>
#include <queue>
#include <string>
int main()
{
std::queue<std::string> playlist;
playlist.push("Crazy");
playlist.push("Gold Digger");
playlist.push("Single Ladies");
while (!playlist.empty()) {
std::cout << "Now playing: " << playlist.front() << "\n";
playlist.pop();
}
return 0;
}Now playing: Crazy
Now playing: Gold Digger
Now playing: Single LadiesKey methods:
void push(const T& value); // add to back
void pop(); // remove from front
T& front(); // access front element
T& back(); // access back element
bool empty() const;
size_t size() const; std::priority_queue
std::priority_queue<T> is a queue where the highest-priority element is always at the top. By default, it is a max-heap — the largest element has the highest priority:
#include <iostream>
#include <queue>
int main()
{
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
while (!pq.empty()) {
std::cout << pq.top() << " ";
pq.pop();
}
std::cout << "\n";
return 0;
}50 30 20 10To get a min-heap (smallest first), use std::greater<T> as the comparator:
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;Choosing the Right Container
Here is a quick decision guide:
| Need | Container |
|---|---|
| Sequential access, dynamic size | std::vector |
| Fixed-size array | std::array |
| Fast lookup by key (sorted) | std::map |
| Fast lookup by key (unsorted) | std::unordered_map |
| Unique values (sorted) | std::set |
| Unique values (unsorted) | std::unordered_set |
| Duplicate keys allowed (sorted) | std::multimap / std::multiset |
| LIFO (stack behavior) | std::stack |
| FIFO (queue behavior) | std::queue |
| Priority-based access | std::priority_queue |
When in doubt, start with std::vector. It has the best cache performance and works well for most tasks. Switch to a different container only when you have a specific need that std::vector does not serve well.
Tip: std::vector is almost always faster than you expect. Even linear search through a small vector often beats hash table or tree lookup because of CPU cache effects. Profile before switching containers.
Try It: Container Sampler
Here is a program that exercises multiple container types. Type it in, compile with g++ -std=c++23, and experiment:
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
int main()
{
// map: album ratings
std::map<std::string, double> ratings;
ratings["Hot Fuss"] = 4.5;
ratings["Elephant"] = 4.8;
ratings["Funeral"] = 4.7;
ratings["X&Y"] = 3.9;
std::cout << "Albums (sorted):\n";
for (const auto& [album, rating] : ratings) {
std::cout << " " << album << ": " << rating << "\n";
}
// set: unique genres
std::set<std::string> genres = {"Rock", "Indie", "Pop", "Rock", "Indie"};
std::cout << "\nGenres: ";
for (const auto& g : genres) {
std::cout << g << " ";
}
std::cout << "\n";
// unordered_map: fast lookup
std::unordered_map<std::string, int> track_num;
track_num["Intro"] = 1;
track_num["Verse"] = 2;
track_num["Chorus"] = 3;
if (track_num.contains("Chorus")) {
std::cout << "\nChorus is track " << track_num["Chorus"] << "\n";
}
// stack: undo history
std::stack<std::string> undo;
undo.push("add track");
undo.push("change volume");
undo.push("delete track");
std::cout << "\nUndo stack:\n";
while (!undo.empty()) {
std::cout << " undo: " << undo.top() << "\n";
undo.pop();
}
// priority_queue: highest rated first
std::priority_queue<std::pair<double, std::string>> top_albums;
for (const auto& [album, rating] : ratings) {
top_albums.push({rating, album});
}
std::cout << "\nTop albums:\n";
while (!top_albums.empty()) {
auto [rating, album] = top_albums.top();
std::cout << " " << album << " (" << rating << ")\n";
top_albums.pop();
}
return 0;
}Try adding a std::multimap that maps genres to multiple songs. Experiment with std::unordered_set and see how the iteration order differs from std::set.
Key Points
- Associative containers (
std::map,std::set,std::multimap,std::multiset) store elements in sorted order using balanced trees, with O(log n) operations. std::mapstores unique key-value pairs;std::setstores unique values only.std::multimapandstd::multisetallow duplicate keys.- Unordered containers (
std::unordered_map,std::unordered_set) use hash tables for O(1) average-case operations, but elements have no guaranteed order. - Use
contains()(C++20) orfind()to check for existence without inserting.operator[]on amapinserts a default value if the key is missing. - Container adaptors (
std::stack,std::queue,std::priority_queue) wrap other containers to provide restricted interfaces. stackis LIFO,queueis FIFO,priority_queueis a max-heap by default.pop()on adaptors does not return the removed element — calltop()orfront()first.- When choosing a container, start with
std::vectorand switch only when you have a specific need. - Use structured bindings (
auto [key, value]) to iterate over maps concisely.
Exercises
Think about it: Why does
std::map::operator[]insert a default value when the key is missing, instead of throwing an exception? What would the implications be if it threw instead?What does this print?
std::map<std::string, int> m; m["b"] = 2; m["a"] = 1; m["c"] = 3; for (const auto& [k, v] : m) { std::cout << k << v; } std::cout << "\n";Where is the bug?
std::map<std::string, int> scores; scores["Alice"] = 95; scores["Bob"] = 87; int charlie_score = scores["Charlie"]; std::cout << "Charlie: " << charlie_score << "\n";What does this print?
std::set<int> s = {5, 3, 1, 4, 1, 5, 3}; std::cout << s.size() << "\n";Calculation: A
std::map<std::string, int>has 1,000,000 entries. Approximately how many comparisons does a lookup require? (Hint: O(log n) with base 2.)Think about it: When would you choose
std::mapoverstd::unordered_map? Give two concrete scenarios.What does this print?
std::stack<int> s; s.push(10); s.push(20); s.push(30); s.pop(); std::cout << s.top() << "\n"; s.pop(); std::cout << s.top() << "\n";Where is the bug?
std::priority_queue<int> pq; pq.push(5); pq.push(15); pq.push(10); int smallest = pq.top(); std::cout << "Smallest: " << smallest << "\n";What does this print?
std::multiset<int> ms = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}; std::cout << ms.count(5) << "\n";Write a program that reads words from the user (one per line, until they type “done”) and uses a
std::map<std::string, int>to count how many times each word was entered. Print the word counts in alphabetical order.