C++ STL Introduction
The C++ Standard Template Library (STL) is a collection of ready-made, highly optimized generic components: containers that store data, iterators that traverse that data, and algorithms that operate on it. Instead of hand-writing a dynamic array, a linked list, or a sorting routine every time you need one, the STL gives you battle-tested, template-based building blocks that work with almost any data type. Nearly every serious C++ program relies on the STL, so understanding it is essential to writing modern, idiomatic C++.
Overview / How the STL Works
The STL is built from four cooperating pieces:
- Containers — class templates that store collections of objects, such as
std::vector,std::map,std::set, andstd::list. Because they are templates, the samevectorcode works forint,std::string, or a custom class, with the actual type substituted at compile time (no runtime overhead). - Iterators — objects that behave like generalized pointers. They let algorithms walk through a container’s elements (
begin()toend()) without needing to know whether the underlying structure is a contiguous array or a linked set of nodes. - Algorithms — free functions such as
std::sort,std::find, andstd::accumulatethat operate purely through iterator ranges. Because algorithms never touch a container directly, onesortfunction works on avector, adeque, or a plain C array. - Function objects (functors) and lambdas — callable objects passed to algorithms to customize behavior, such as a custom comparison for sorting.
This separation is the key design idea: containers don’t know about algorithms, and algorithms don’t know about containers — iterators are the glue between them. This is sometimes called the STL’s “generic programming” model, and it is implemented entirely through C++ templates, which are expanded and type-checked at compile time. That means there is no virtual-function overhead just to use a vector or call sort: the compiler generates specialized machine code for each concrete type you use.
Under the hood, a container like std::vector manages memory on your behalf. It allocates a contiguous block on the heap, tracks a size (how many elements are actually stored) and a capacity (how much space is currently allocated). When you push_back past the current capacity, the vector allocates a new, larger block (typically doubling in size), moves or copies the existing elements over, and frees the old block. This amortized-growth strategy is why appending to a vector is, on average, a constant-time operation even though occasional reallocations are linear-time. Associative containers like std::map and std::set, by contrast, are usually implemented as self-balancing binary search trees (typically red-black trees), which is why their elements are always kept in sorted key order and lookups cost O(log n).
Syntax
Every STL container follows the same general declaration pattern:
container_name<element_type> variable_name;
container_name<key_type, value_type> variable_name; // for maps
| Container | Header | Description |
|---|---|---|
vector |
vector | Dynamic array; fast random access and append at the end |
list |
list | Doubly linked list; fast insert/erase anywhere, no random access |
deque |
deque | Double-ended queue; fast insert/erase at both ends |
set / multiset |
set | Sorted, unique (or non-unique) keys, tree-based |
map / multimap |
map | Sorted key-value pairs, tree-based |
unordered_map |
unordered_map | Hash-table based key-value pairs, average O(1) lookup |
stack, queue |
stack, queue | Container adapters restricting access to LIFO/FIFO order |
Algorithms follow a similar pattern — they take a range described by two iterators, plus optional extra arguments:
algorithm_name(container.begin(), container.end(), /* extra args */);
container.begin()— an iterator pointing to the first element.container.end()— an iterator pointing one past the last element (never dereferenced).- Extra arguments vary by algorithm: a value to search for, a comparator function, an output iterator, and so on.
Examples
Example 1: std::vector basics
#include <iostream>
#include <vector>
#include <numeric>
int main() {
std::vector<int> scores;
scores.push_back(85);
scores.push_back(92);
scores.push_back(78);
scores.push_back(90);
std::cout << "Number of scores: " << scores.size() << std::endl;
for (int score : scores) {
std::cout << score << " ";
}
std::cout << std::endl;
int total = std::accumulate(scores.begin(), scores.end(), 0);
double average = static_cast<double>(total) / scores.size();
std::cout << "Average: " << average << std::endl;
return 0;
}
Output:
Number of scores: 4
85 92 78 90
Average: 86.25
A std::vector<int> starts empty; each push_back appends a new element, growing the underlying array as needed. The range-based for loop uses iterators internally to visit every element, and std::accumulate (from <numeric>) sums a range in one call instead of a manual loop.
Example 2: std::map for counting
#include <iostream>
#include <map>
#include <string>
#include <sstream>
int main() {
std::string text = "the quick brown fox jumps over the lazy dog the fox runs";
std::map<std::string, int> wordCount;
std::stringstream ss(text);
std::string word;
while (ss >> word) {
wordCount[word]++;
}
for (const auto& pair : wordCount) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Output:
brown: 1
dog: 1
fox: 2
jumps: 1
lazy: 1
over: 1
quick: 1
runs: 1
the: 3
This is one of the most useful STL patterns: use a map<std::string, int> as a frequency table. wordCount[word]++ looks up the key; if it doesn’t exist yet, operator[] default-constructs it (an int defaults to 0), then increments it. Because std::map stores keys in sorted order internally, iterating over it always produces output in alphabetical order — no separate sort step needed.
Example 3: Algorithms with std::sort, std::binary_search, and std::set
#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
int main() {
std::vector<int> numbers = {5, 3, 8, 3, 9, 1, 5, 8, 2};
std::sort(numbers.begin(), numbers.end());
std::cout << "Sorted: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;
bool found = std::binary_search(numbers.begin(), numbers.end(), 8);
std::cout << "Contains 8? " << (found ? "yes" : "no") << std::endl;
std::set<int> uniqueNumbers(numbers.begin(), numbers.end());
std::cout << "Unique count: " << uniqueNumbers.size() << std::endl;
return 0;
}
Output:
Sorted: 1 2 3 3 5 5 8 8 9
Contains 8? yes
Unique count: 6
Here, std::sort reorders the vector in place using an efficient introsort (a hybrid of quicksort, heapsort, and insertion sort). Once sorted, std::binary_search can check for a value in O(log n) time. Finally, constructing a std::set<int> directly from the vector’s iterator range automatically discards duplicates, since a set only ever stores unique keys.
How It Works Step by Step
- You declare a container by instantiating its template with a concrete type, e.g.
vector<int>; the compiler generates a specialized class for that type. - The container manages its own memory:
vectorallocates a contiguous heap block and grows it (usually by doubling) when it runs out of capacity; tree-based containers likemapallocate individual nodes and link them together, keeping keys sorted as they’re inserted. - An iterator is requested from the container (
begin()/end()); it knows how to move to the “next” element for that specific container’s internal layout. - An algorithm receives a pair of iterators and repeatedly dereferences and advances them, remaining completely unaware of whether it’s walking an array or a tree.
- When the container goes out of scope, its destructor automatically releases all owned memory — no manual
deleteis required, thanks to RAII (Resource Acquisition Is Initialization).
Common Mistakes
Mistake 1: Confusing std::array with std::vector
std::array has a fixed size decided at compile time and offers no way to add elements after creation. Calling push_back on it is a compile error, not a runtime one:
#include <iostream>
#include <array>
int main() {
std::array<int, 3> numbers = {1, 2, 3};
numbers.push_back(4); // error: no member named 'push_back' in 'std::array'
for (int n : numbers) std::cout << n << " ";
return 0;
}
The fix is to use std::vector when the number of elements can change at runtime:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3};
numbers.push_back(4);
for (int n : numbers) std::cout << n << " ";
std::cout << std::endl;
return 0;
}
Output:
1 2 3 4
std::array is a thin wrapper over a fixed-size C-style array and should only be used when the size is truly known and constant.
Mistake 2: Using operator[] to “check” whether a map key exists
std::map::operator[] silently inserts a default-constructed value if the key isn’t already present. Using it just to test existence corrupts the map:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> inventory;
inventory["apples"] = 10;
if (inventory["bananas"] > 0) {
std::cout << "We have bananas" << std::endl;
} else {
std::cout << "No bananas" << std::endl;
}
std::cout << "Map size: " << inventory.size() << std::endl;
return 0;
}
Output:
No bananas
Map size: 2
Even though “bananas” was never explicitly added, the map now has two entries, because operator[] inserted "bananas" -> 0 the moment it was accessed. The fix is to use count() or find(), which never modify the map:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> inventory;
inventory["apples"] = 10;
if (inventory.count("bananas") > 0) {
std::cout << "We have bananas" << std::endl;
} else {
std::cout << "No bananas" << std::endl;
}
std::cout << "Map size: " << inventory.size() << std::endl;
return 0;
}
Output:
No bananas
Map size: 1
Best Practices
- Default to
std::vectorunless you have a specific reason (frequent middle insertion/removal, need for sorted keys, etc.) to use another container. - Prefer range-based
forloops over manual iterator loops when you don’t need the iterator itself — they’re shorter and harder to get wrong. - Use
.at(index)instead ofoperator[]when you want bounds checking (it throwsstd::out_of_rangeon an invalid index, whereasoperator[]is undefined behavior). - Pass containers by
const&to functions that only read them, to avoid unnecessary copies. - Reserve capacity with
vector::reserve(n)up front when you know roughly how many elements you’ll insert, to avoid repeated reallocations. - Reach for an algorithm (
std::sort,std::find,std::count,std::accumulate, etc.) before writing a manual loop — they’re well-tested and communicate intent clearly. - Use
unordered_map/unordered_setinstead ofmap/setwhen you don’t need sorted order and want faster average-case lookups.
Practice Exercises
- Write a program that reads a sentence into a
std::vector<std::string>of words (split on spaces) and prints the longest word. - Build a
std::map<char, int>that counts how many times each letter appears in a given string (ignore spaces), then print the counts in alphabetical order. - Given a
std::vector<int>of exam scores, usestd::sortandstd::max_element/std::min_elementto print the sorted list along with the highest and lowest score.
Summary
- The STL provides three cooperating pieces: containers (data storage), iterators (traversal), and algorithms (operations), glued together through templates.
std::vectoris a dynamic, contiguous array that grows by reallocating and copying when its capacity is exceeded.std::mapandstd::setare typically tree-based, keeping elements in sorted key order with O(log n) operations.- Algorithms like
std::sort,std::find, andstd::accumulatework through iterator ranges, so the same algorithm works across many container types. - Common pitfalls include mixing up fixed-size
std::arraywith resizablestd::vector, and usingmap::operator[]for existence checks (usecount()orfind()instead). - Favor STL containers and algorithms over hand-rolled data structures and loops — they are safer, well-tested, and usually just as fast or faster.
