STL in C++ (Standard Template Library) is a powerful collection of generic containers, iterators, algorithms, function objects, and utilities that help you write clean, fast, and reusable code. This beginner-friendly guide walks you through the essentials with commented code, outputs, and “Try it yourself” challenges after every section.
🔹 What is STL in C++?
The STL in C++ is a set of standard, generic building blocks that handle common tasks like storing data, traversing it, transforming it, and searching efficiently—without you rewriting these from scratch. It’s highly optimized and type-safe thanks to templates.
- Containers: structures to hold data (e.g.,
vector
,map
,set
). - Iterators: generic pointers to traverse containers.
- Algorithms: reusable functions (e.g.,
sort
,find
,accumulate
). - Function objects/lambdas: custom behavior you pass to algorithms.
- Utilities: helpers like
pair
,tuple
,optional
,string
.
Try it yourself
- Name a task you do often (sorting, counting, deduplicating). Which STL components might help (container + algorithm)?
- Create a tiny list of integers and think through how you’d sort and remove duplicates with STL calls only.
🔹 STL Containers at a Glance
Pick the container based on your access and performance needs. Common groups:
- Sequence containers:
vector
,deque
,list
,array
,forward_list
- Ordered associative:
set
,multiset
,map
,multimap
(sorted by key) - Unordered associative:
unordered_set
,unordered_map
(hash tables) - Adapters:
stack
,queue
,priority_queue
Rule of thumb: start with vector
for dynamic arrays. Switch when you have a clear need for ordered keys (use map
/set
) or fast hashed lookups (use unordered_map
/unordered_set
).
Try it yourself
- Select the best container for: a) top scores list, b) phonebook lookup by name, c) count unique words.
- Explain why
vector
is usually faster thanlist
in practice.
🔹 vector: Your Dynamic Array
std::vector
is the go-to container for contiguous, dynamic arrays with amortized O(1) push back and random access in O(1).
#include <iostream>
#include <vector>
#include <algorithm> // sort, reverse
using namespace std;
int main() {
vector<int> a = {5, 1, 4, 2, 3};
a.push_back(6); // add at end (amortized O(1))
sort(a.begin(), a.end()); // O(n log n) sort
reverse(a.begin(), a.end()); // reverse order
// Print using range-based for
for (int x : a)
{ cout << x << " "; }
cout << "\n";
// Random access
cout << "Front=" << a.front()
<< ", Back=" << a.back()
<< ", a=" << a[2] << "\n";
}
Output
6 5 4 3 2 1
Front=6, Back=1, a=4
Tip: Reserve capacity with v.reserve(n)
if you know the size in advance to reduce reallocations.
Try it yourself
- Create
vector<string>
of names. Sort alphabetically and print. - Measure difference between using
reserve
vs not when pushing 1e6 integers (just timing, no need to benchmark rigorously).
🔹 Iterators: Moving Through Containers
Iterators act like generalized pointers. Algorithms use them to traverse containers without knowing the container type.
#include <iostream>
#include <vector>
#include <iterator> // back_inserter
#include <algorithm>
using namespace std;
int main() {
vector<int> src = {1, 2, 3, 4, 5};
vector<int> dst;
// Use back_inserter to append via iterator interface
transform(src.begin(), src.end(), back_inserter(dst), [](int x)
{ return x * x; });
// square each
// Iterate with explicit iterators
for (auto it = dst.begin(); it != dst.end(); ++it)
{ cout << *it << " "; }
cout << "\n";
}
Output
1 4 9 16 25
Iterator categories: input, output, forward, bidirectional, random-access. Most sequences like vector
offer random-access iterators; list
offers bidirectional.
Try it yourself
- Create a
vector<int>
and useauto rit = v.rbegin()
to print in reverse. - Use
std::advance
to move an iterator forward by 3 and print the element.
🔹 Algorithms: No Loops Required
Algorithms are free functions that operate on iterator ranges. Many take a predicate (lambda) to customize behavior.
#include <iostream>
#include <vector>
#include <algorithm> // sort, count_if, remove_if, transform
#include <numeric> // accumulate
#include <cctype>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6};
// sum with accumulate
int sum = accumulate(v.begin(), v.end(), 0);
// count even numbers
int evens = count_if(v.begin(), v.end(), [](int x)
{ return x % 2 == 0; });
// transform to double each element (in place)
transform(v.begin(), v.end(), v.begin(), [](int x)
{ return x * 2; });
cout << "sum=" << sum
<< ", evens=" << evens << "\n";
for (int x : v) cout << x << " ";
cout << "\n";
}
Output
sum=21, evens=3
2 4 6 8 10 12
Common algorithms: sort
, reverse
, unique
, remove_if
, find
, binary_search
, lower_bound
, accumulate
, transform
, rotate
, shuffle
.
Try it yourself
- Sort a vector descending using a custom comparator lambda.
- Use
unique
+erase
to remove consecutive duplicates (see the erase-remove idiom below).
🔹 map and unordered_map: Key–Value Lookups
map
is ordered (balanced tree); unordered_map
is hashed (average O(1) lookup). Use them to associate keys with values (like a dictionary).
#include <iostream>
#include <unordered_map>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> words = {"apple","banana","apple","pear","banana","apple"};
unordered_map<string, int> freq;
// Count frequencies
for (const auto& w : words) { ++freq[w]; }
// Print
for (const auto& [k, v] : freq) {
cout << k << ": "
<< v << "\n";
}
// Find
if (auto it = freq.find("banana"); it != freq.end()) {
cout << "banana freq = "
<< it->second << "\n";
}
}
Possible Output
pear: 1
banana: 2
apple: 3
banana freq = 2
Use map
if you need sorted traversal by key. Use unordered_map
for fastest average lookups without order guarantees.
Try it yourself
- Switch to
map
and observe sorted output by key. - Insert with
freq.emplace("grape", 1)
and check if it exists before inserting.
🔹 set and unordered_set: Unique Items
set
maintains unique, sorted elements. unordered_set
maintains unique elements in a hash table (no order).
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
// allows duplicates
multiset<string> ms = {"b","a","b","c","a"};
// removes duplicates, keeps order
set<string> unique(ms.begin(), ms.end());
for (const auto& s : unique)
cout << s << " ";
cout << "\n";
// Check membership
cout << (unique.count("b") ? "has b" : "no b") << "\n";
}
Output
a b c
has b
Use unordered_set
for fast membership tests; use set
when you also need ordered iteration.
Try it yourself
- Create an
unordered_set<int>
, insert random values, and test membership withcount()
. - Compare iteration order between
set
andunordered_set
.
🔹 Stack, Queue, Priority Queue (Adapters)
Container adapters give you specific interfaces (LIFO/FIFO/priority) on top of underlying containers (default: deque
or vector
for priority_queue
).
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
int main() {
stack<int> st;
st.push(1); st.push(2); st.push(3);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << "\n";
queue<int> q;
q.push(10); q.push(20); q.push(30);
while (!q.empty()) {
cout << q.front() << " ";
q.pop();
}
cout << "\n";
// max-heap by default
priority_queue<int> pq;
for (int x : {4, 1, 7, 3}) pq.push(x);
while (!pq.empty()) {
cout << pq.top() << " ";
pq.pop();
}
cout << "\n";
}
Output
3 2 1
10 20 30
7 4 3 1
Tip: For a min-heap, use priority_queue<int, vector<int>, greater<int>>
.
Try it yourself
- Create a min-heap and push values; confirm smallest comes out first.
- Use
queue
to simulate simple task scheduling.
🔹 Erase–Remove Idiom (Remove by Predicate)
Algorithms don’t change container sizes—so to remove elements by condition from sequence containers, use remove_if
+ erase
.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {1,2,3,4,5,6,7,8,9};
// Remove all odd numbers
v.erase(remove_if(v.begin(), v.end(), [](int x)
{ return x % 2 == 1; }), v.end());
for (int x : v) cout << x << " ";
cout << "\n";
}
Output
2 4 6 8
Remember: remove_if
reorders elements and returns the new “logical end;” erase
actually shrinks the container.
Try it yourself
- Remove all values < 5 from a
vector<int>
. - Use
unique
+erase
after sorting to remove duplicate values.
🔹 Custom Comparators and Lambdas
Algorithms and associative containers accept comparators. Lambdas make it concise to customize behavior (sort order, grouping rules, etc.).
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int main() {
vector<string> names = {"Ann","Elizabeth","Joe","Christopher"};
// Sort by length, then alphabetically for ties
sort(names.begin(), names.end(),
[](const string& a, const string& b){
if (a.size() != b.size()) return a.size() < b.size();
return a < b;
});
for (const auto& s : names)
cout << s << " ";
cout << "\n";
}
Output
Ann Joe Elizabeth Christopher
Use stateless lambdas for quick, inline logic. For reusable logic, write a named functor or function.
Try it yourself
- Sort strings ignoring case (hint:
tolower
each char in the comparator). - Sort numbers by absolute value.
🔹 (Optional) C++20 Ranges with STL
C++20 adds ranges: pipe-friendly views and algorithms. They improve readability by composing filters and transforms before materializing results.
#include <iostream>
#include <vector>
#include <ranges> // C++20
using namespace std;
int main() {
vector<int> v = {1,2,3,4,5,6};
// Filter even numbers and square them lazily
auto view = v
| ranges::views::filter([](int x){ return x % 2 == 0; })
| ranges::views::transform([](int x){ return x * x; });
for (int x : view) cout << x << " ";
cout << "\n";
}
Output
4 16 36
Ranges are great for readable pipelines. They work hand-in-hand with the classic STL.
Try it yourself
- Create a pipeline to take numbers 1..20, keep multiples of 3, then add 1 to each.
- Materialize a view into a
vector
withranges::to<vector>()
if your compiler supports it.
🔹 Mini Project: Top-K Frequent Words
Combine containers, algorithms, and a custom comparator to compute the top-K words by frequency. This showcases practical use of the STL in C++.
#include <iostream>
#include <unordered_map>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
vector<string> topKWords(const vector<string>& words, size_t K)
{
unordered_map<string, int> freq;
for (const auto& w : words) ++freq[w];
// Move to vector for sorting
vector<pair<string,int>> items(freq.begin(), freq.end());
// Sort by freq desc, then word asc
sort(items.begin(), items.end(), [](const auto& a, const auto& b){
if (a.second != b.second) return a.second > b.second;
return a.first < b.first;
});
vector<string> result;
for (size_t i = 0; i < min(K, items.size()); ++i) {
result.push_back(items[i].first);
}
return result;
}
int main() {
vector<string> words = {"a","b","a","c","b","a","d","c","b","b"};
auto top2 = topKWords(words, 2);
for (const auto& w : top2) cout << w << " ";
cout << "\n";
}
Output
b a
You can swap unordered_map
with map
for ordered frequency iteration or use a min-heap (priority_queue
) to stream through data for large inputs.
Try it yourself
- Change the tie-breaker to prefer lexicographically larger words on equal frequency.
- Replace sort with a min-heap of size K to handle very large word streams.
🔹 Best Practices and Performance Tips
- Prefer
vector
by default (cache-friendly, fast). Switch only with clear reasons. - Use
reserve
forvector
andrehash
forunordered_map
when you know sizes. - Use algorithms instead of manual loops for clarity and fewer bugs.
- Use
erase-remove
for sequence removals; don’t erase in the middle of iterating with invalidated iterators. - For custom types in ordered containers, define a strict weak ordering (consistent comparator).
- For
unordered_*
, provide a good hash and equality if you define custom key types. - Use lambdas for one-off logic; named functors for reusable policies.
Try it yourself
- Add
reserve
to your frequency counter and observe reduced rehashes if you logbucket_count()
. - Write a custom
struct Person {string name; int id;}
and store it inset<Person>
with a comparator byid
.
🔹 FAQs about STL in C++
Q1: When should I pick vector
over list
?
Almost always use vector
. It’s contiguous, cache-friendly, and fast. Use list
only if you must splice without moving elements or need stable iterators during frequent mid-container insert/erase.
Try it: Benchmark inserting 100k items at the back for vector
vs list
; then random access—notice vector
wins.
Q2: Why do algorithms take iterators and not containers?
It decouples algorithms from container types, making them reusable for any range defined by two iterators.
Try it: Use the same sort
on a C-array and on a vector
by passing iterators.
Q3: What’s the time complexity of map
vs unordered_map
?map
: O(log n) operations; unordered_map
: average O(1), worst-case O(n). map
keeps keys ordered; unordered_map
does not.
Try it: Insert 10k keys and time lookups for both containers (rough comparison).
Q4: How do I remove elements safely while iterating?
Use the erase–remove idiom for sequences; for associative containers, use iterator-returning erase
or post-increment the iterator before erasing.
Try it: Remove all values < 0 from a vector
with erase–remove; remove all odd keys from a map
with iterator erasure.
Q5: Why does unique
keep one of each duplicate?
It compacts equal adjacent elements into a single one and returns the new end; you must call erase
to shrink.
Try it: Sort a vector with duplicates, call unique
, then erase
, and print the result.
Q6: Can I use custom types as keys in unordered_map
?
Yes, if you provide hash<T>
and equal_to<T>
specializations or custom functors.
Try it: Make Point{x,y}
hashable and use it as a key in unordered_map<Point,int>
.
Q7: What invalidates iterators?
For vector
, reallocation (e.g., growth) invalidates all iterators; inserting/erasing in the middle invalidates from the point onward. For list
, most operations keep iterators valid.
Try it: Keep an iterator to a vector
element, push_back until capacity changes, and test if accessing through the old iterator is unsafe (don’t dereference in production).
Q8: Should I use using namespace std;
with STL?
Prefer qualified names or selective using
declarations to avoid name clashes (especially in headers).
Try it: Replace using namespace std;
with using std::string;
and explicit std::
qualifiers in a small program.
Q9: Are STL algorithms parallelizable?
C++17 adds parallel execution policies (<execution>
) for some algorithms; availability and performance depend on your toolchain and data sizes.
Try it: Experiment with std::sort(std::execution::par, ...)
on a large dataset (if supported).
Q10: Do I need to learn ranges now?
Classic STL is enough to be productive. Ranges improve composability and readability—adopt gradually if your compiler supports C++20.
Try it: Rewrite a filter+transform loop using ranges and compare readability.
🔹 Wrapping Up
The STL in C++ gives you fast, reliable building blocks: containers to store data, iterators to traverse, algorithms to transform, and adapters/utilities to tailor behavior. Start with vector
, lean on algorithms and lambdas, and apply best practices like erase–remove and reserving capacity. Work through the “Try it yourself” tasks to make STL usage second nature.