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
vectoris usually faster thanlistin 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=4Tip: 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
reservevs 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::advanceto 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+eraseto 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 = 2Use map if you need sorted traversal by key. Use unordered_map for fastest average lookups without order guarantees.
Try it yourself
- Switch to
mapand 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 bUse 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
setandunordered_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 1Tip: 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
queueto 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 8Remember: 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+eraseafter 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 ChristopherUse stateless lambdas for quick, inline logic. For reusable logic, write a named functor or function.
Try it yourself
- Sort strings ignoring case (hint:
tolowereach 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 36Ranges 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
vectorwithranges::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 aYou 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
vectorby default (cache-friendly, fast). Switch only with clear reasons. - Use
reserveforvectorandrehashforunordered_mapwhen you know sizes. - Use algorithms instead of manual loops for clarity and fewer bugs.
- Use
erase-removefor 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
reserveto 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.