STL in C++: Complete Guide with Containers, Iterators and Algorithms

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 than list 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 use auto 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 with count().
  • Compare iteration order between set and unordered_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 with ranges::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 for vector and rehash for unordered_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 log bucket_count().
  • Write a custom struct Person {string name; int id;} and store it in set<Person> with a comparator by id.

🔹 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.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.