The Standard Template Library (STL): Your C++ Toolkit

Master the C++ Standard Template Library (STL). Learn containers, iterators, and algorithms—vector, map, sort, find—with practical examples and step-by-step explanations.

The Standard Template Library (STL): Your C++ Toolkit

The C++ Standard Template Library (STL) is a powerful collection of generic classes and functions that provides ready-to-use data structures (containers), algorithms, and iterators. Built on template programming, the STL includes containers like vector, list, map, and set; algorithms like sort, find, and transform; and iterators that connect containers and algorithms—enabling you to write efficient, reusable code without reinventing fundamental programming building blocks.

Introduction: Standing on the Shoulders of Giants

Before the STL, every C++ programmer had to implement their own linked lists, dynamic arrays, sorting algorithms, and search routines. This was error-prone, time-consuming, and led to inconsistent quality across codebases. The Standard Template Library changed everything by providing a comprehensive, tested, and highly optimized collection of data structures and algorithms that work together seamlessly.

The STL is not just a library—it’s a design philosophy. It demonstrates how template programming can create generic, type-safe, and efficient abstractions. When you use vector<int> instead of a raw array, sort() instead of a hand-written sorting loop, or map<string, int> instead of a custom hash table, you’re leveraging decades of computer science knowledge packaged into a clean, consistent interface.

Three fundamental concepts unite the STL: containers (objects that store data), algorithms (functions that operate on data), and iterators (objects that provide access to container elements). These three components are designed to work together but remain independent—any iterator-compatible container can be used with any iterator-compatible algorithm. This orthogonal design is what makes the STL so powerful and flexible.

This comprehensive guide will walk you through the essential components of the STL, showing you how containers, algorithms, and iterators work individually and together. You’ll learn through practical examples that demonstrate real-world usage, understand the design principles behind the STL, and gain the knowledge you need to leverage this toolkit effectively in your own programs.

The Three Pillars of the STL

The STL is built around three interconnected concepts that work together as a unified system.

C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    // PILLAR 1: CONTAINER - stores data
    vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    
    cout << "=== Original Data (Container) ===" << endl;
    for (int n : numbers) {
        cout << n << " ";
    }
    cout << endl;

    // PILLAR 2: ITERATOR - provides access to elements
    cout << "\n=== Using Iterators to Access Data ===" << endl;
    for (vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // PILLAR 3: ALGORITHM - operates on data via iterators
    cout << "\n=== After sort() Algorithm ===" << endl;
    sort(numbers.begin(), numbers.end());
    for (int n : numbers) {
        cout << n << " ";
    }
    cout << endl;

    // All three working together
    cout << "\n=== Finding a Value (all three together) ===" << endl;
    auto it = find(numbers.begin(), numbers.end(), 7);
    if (it != numbers.end()) {
        cout << "Found 7 at position: " << distance(numbers.begin(), it) << endl;
    }

    return 0;
}

Step-by-step explanation:

  1. vector<int> numbers: The container — stores a collection of integers in dynamic memory
  2. Initializer list: {5, 2, 8, 1, 9, 3, 7, 4, 6} provides initial values at construction
  3. Range-based for loop: Modern syntax that hides iterator complexity
  4. vector<int>::iterator: The iterator type — a pointer-like object that navigates the container
  5. numbers.begin(): Returns iterator pointing to the first element
  6. numbers.end(): Returns iterator pointing just past the last element (sentinel)
  7. *it: Dereferences the iterator to get the element value
  8. ++it: Advances iterator to the next element
  9. sort(): An algorithm — takes two iterators as boundaries and sorts the range
  10. Decoupled design: sort() doesn’t know or care that numbers is a vector
  11. find(): Another algorithm — searches for value 7 between begin() and end()
  12. Return value check: find() returns end() if element not found
  13. distance(): Calculates how many steps between two iterators (i.e., the index)
  14. Three pillars unified: All three concepts connect through the iterator interface

Output:

Plaintext
=== Original Data (Container) ===
5 2 8 1 9 3 7 4 6

=== Using Iterators to Access Data ===
5 2 8 1 9 3 7 4 6

=== After sort() Algorithm ===
1 2 3 4 5 6 7 8 9

=== Finding a Value (all three together) ===
Found 7 at position: 6

STL Containers: Choosing the Right Storage

The STL offers a range of containers, each with different performance trade-offs. Choosing the right container is key to writing efficient code.

C++
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <string>
using namespace std;

int main() {
    // --- vector: dynamic array, O(1) random access ---
    cout << "=== vector ===" << endl;
    vector<int> vec;
    vec.push_back(10);
    vec.push_back(20);
    vec.push_back(30);
    vec.insert(vec.begin() + 1, 15);  // Insert at position 1

    cout << "Contents: ";
    for (int v : vec) cout << v << " ";
    cout << endl;
    cout << "Element at index 2: " << vec[2] << endl;
    cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << endl;

    // --- list: doubly-linked list, O(1) insert/delete anywhere ---
    cout << "\n=== list ===" << endl;
    list<string> lst;
    lst.push_back("Banana");
    lst.push_front("Apple");
    lst.push_back("Cherry");

    cout << "Contents: ";
    for (const string& s : lst) cout << s << " ";
    cout << endl;

    // Efficient insert in the middle
    auto it = lst.begin();
    advance(it, 1);              // Move iterator to index 1
    lst.insert(it, "Apricot");   // O(1) insert at iterator position

    cout << "After insert: ";
    for (const string& s : lst) cout << s << " ";
    cout << endl;

    // --- deque: double-ended queue, O(1) push/pop both ends ---
    cout << "\n=== deque ===" << endl;
    deque<int> dq;
    dq.push_back(3);
    dq.push_front(1);
    dq.push_back(4);
    dq.push_front(0);

    cout << "Contents: ";
    for (int d : dq) cout << d << " ";
    cout << endl;

    dq.pop_front();
    dq.pop_back();
    cout << "After pop_front and pop_back: ";
    for (int d : dq) cout << d << " ";
    cout << endl;

    return 0;
}

Step-by-step explanation:

  1. vector<int> vec: Contiguous array that grows dynamically as elements are added
  2. push_back(): Adds element to the end — O(1) amortized (occasionally triggers resize)
  3. vec.insert(vec.begin() + 1, 15): Inserts 15 at index 1 — O(n) because elements shift right
  4. vec[2]: Random access by index — O(1), very fast due to contiguous memory
  5. capacity vs size: Size is elements used; capacity is total allocated memory (can differ)
  6. list<string> lst: Doubly-linked list — each node has pointers to previous and next
  7. push_front(): Adds to the front — O(1) for list (expensive O(n) for vector)
  8. advance(it, 1): Moves iterator forward by 1 step (list iterators can’t use + like vector)
  9. lst.insert(it, “Apricot”): O(1) insertion once you have the iterator position
  10. deque<int> dq: Double-ended queue — efficient O(1) push/pop at both ends
  11. push_front() on deque: Efficient — deque is internally a segmented array
  12. pop_front() and pop_back(): Remove elements from both ends in O(1)
  13. Container choice matters: Vector for random access, list for frequent midpoint inserts, deque for both-ends operations

Output:

Plaintext
=== vector ===
Contents: 10 15 20 30
Element at index 2: 20
Size: 4, Capacity: 8

=== list ===
Contents: Apple Banana Cherry
After insert: Apple Apricot Banana Cherry

=== deque ===
Contents: 0 1 3 4
After pop_front and pop_back: 1 3

Associative Containers: Key-Based Lookup

Associative containers organize data by keys, enabling fast lookup, insertion, and deletion.

C++
#include <iostream>
#include <map>
#include <set>
#include <unordered_map>
#include <string>
using namespace std;

int main() {
    // --- map: sorted key-value pairs, O(log n) operations ---
    cout << "=== map ===" << endl;
    map<string, int> scores;
    scores["Alice"]   = 95;
    scores["Bob"]     = 82;
    scores["Charlie"] = 91;
    scores["Diana"]   = 88;

    cout << "All scores (sorted by key):" << endl;
    for (const auto& pair : scores) {
        cout << "  " << pair.first << ": " << pair.second << endl;
    }

    // Lookup
    cout << "Bob's score: " << scores["Bob"] << endl;

    // Check existence before accessing
    if (scores.count("Eve") == 0) {
        cout << "Eve not found in map" << endl;
    }

    // --- set: sorted unique values, O(log n) operations ---
    cout << "\n=== set ===" << endl;
    set<int> uniqueNumbers;
    uniqueNumbers.insert(5);
    uniqueNumbers.insert(3);
    uniqueNumbers.insert(8);
    uniqueNumbers.insert(3);   // Duplicate — silently ignored
    uniqueNumbers.insert(1);

    cout << "Unique numbers (sorted): ";
    for (int n : uniqueNumbers) cout << n << " ";
    cout << endl;

    cout << "Contains 3? " << (uniqueNumbers.count(3) ? "Yes" : "No") << endl;
    cout << "Contains 7? " << (uniqueNumbers.count(7) ? "Yes" : "No") << endl;

    // --- unordered_map: hash table, O(1) average operations ---
    cout << "\n=== unordered_map ===" << endl;
    unordered_map<string, string> capitals;
    capitals["France"]  = "Paris";
    capitals["Germany"] = "Berlin";
    capitals["Japan"]   = "Tokyo";
    capitals["USA"]     = "Washington D.C.";

    cout << "Capital of Japan: " << capitals["Japan"] << endl;
    cout << "Capital of Germany: " << capitals["Germany"] << endl;

    cout << "All capitals (unordered):" << endl;
    for (const auto& [country, capital] : capitals) {
        cout << "  " << country << " -> " << capital << endl;
    }

    return 0;
}

Step-by-step explanation:

  1. map<string, int>: Sorted associative container; keys are always in ascending order
  2. operator[] for insertion: scores["Alice"] = 95 inserts or updates the key “Alice”
  3. pair.first / pair.second: Each map element is a std::pair — first is key, second is value
  4. Automatic sorting: Map iterates in alphabetical order regardless of insertion order
  5. scores.count(“Eve”): Returns 0 if key doesn’t exist, 1 if it does (map has unique keys)
  6. set<int>: Stores only unique values, automatically sorted
  7. insert() on duplicate: Silently ignored — set enforces uniqueness
  8. O(log n) operations: Map and set use red-black trees internally
  9. set.count(): Returns 1 if element exists, 0 otherwise — safe existence check
  10. unordered_map<string, string>: Hash map — no ordering, but O(1) average lookup
  11. Hash-based lookup: Uses hash function on key to find bucket directly
  12. Structured binding: const auto& [country, capital] (C++17) unpacks pair neatly
  13. Order not guaranteed: unordered_map iterates in implementation-defined order
  14. When to choose: map for sorted output or range queries; unordered_map for fastest lookup

Output:

Plaintext
=== map ===
All scores (sorted by key):
  Alice: 95
  Bob: 82
  Charlie: 91
  Diana: 88
Bob's score: 82
Eve not found in map

=== set ===
Unique numbers (sorted): 1 3 5 8
Contains 3? Yes
Contains 7? No

=== unordered_map ===
Capital of Japan: Tokyo
Capital of Germany: Berlin
All capitals (unordered):
  USA -> Washington D.C.
  Japan -> Tokyo
  Germany -> Berlin
  France -> Paris

Container Adapters: Specialized Interfaces

Container adapters wrap existing containers to provide restricted, specialized interfaces.

C++
#include <iostream>
#include <stack>
#include <queue>
#include <priority_queue>
#include <string>
using namespace std;

int main() {
    // --- stack: LIFO (Last In, First Out) ---
    cout << "=== stack (LIFO) ===" << endl;
    stack<string> callStack;

    callStack.push("main()");
    callStack.push("calculateTax()");
    callStack.push("getRate()");

    cout << "Call stack (top to bottom):" << endl;
    while (!callStack.empty()) {
        cout << "  " << callStack.top() << endl;
        callStack.pop();
    }

    // --- queue: FIFO (First In, First Out) ---
    cout << "\n=== queue (FIFO) ===" << endl;
    queue<string> printQueue;

    printQueue.push("Document1.pdf");
    printQueue.push("Report.docx");
    printQueue.push("Invoice.pdf");

    cout << "Processing print queue:" << endl;
    while (!printQueue.empty()) {
        cout << "  Printing: " << printQueue.front() << endl;
        printQueue.pop();
    }

    // --- priority_queue: highest priority out first ---
    cout << "\n=== priority_queue (max-heap by default) ===" << endl;
    priority_queue<int> pq;

    pq.push(30);
    pq.push(10);
    pq.push(50);
    pq.push(20);
    pq.push(40);

    cout << "Dequeuing by priority (highest first):" << endl;
    while (!pq.empty()) {
        cout << "  " << pq.top() << endl;
        pq.pop();
    }

    // Min-heap: use greater<int> comparator
    cout << "\n=== priority_queue (min-heap) ===" << endl;
    priority_queue<int, vector<int>, greater<int>> minPQ;
    minPQ.push(30);
    minPQ.push(10);
    minPQ.push(50);
    minPQ.push(20);

    cout << "Dequeuing by priority (lowest first):" << endl;
    while (!minPQ.empty()) {
        cout << "  " << minPQ.top() << endl;
        minPQ.pop();
    }

    return 0;
}

Step-by-step explanation:

  1. stack<string>: Adapts an underlying container (deque by default) into a LIFO interface
  2. push(): Adds element on top of the stack
  3. top(): Reads the topmost element without removing it
  4. pop(): Removes the topmost element (does NOT return it — check top() first)
  5. LIFO behavior: getRate() was pushed last, so it comes off first — models a call stack
  6. queue<string>: FIFO container — first element pushed is the first one dequeued
  7. front(): Reads the element at the front (oldest element)
  8. FIFO behavior: Document1.pdf was pushed first, so it prints first — models a print queue
  9. priority_queue<int>: Max-heap by default — largest element always at top
  10. push() reorders internally: After each push, heap property is maintained automatically
  11. top() on priority_queue: Always returns the highest priority (max) element
  12. greater<int> comparator: Reverses the ordering, creating a min-heap
  13. Container adapters hide complexity: You can’t iterate over stack/queue — intentional restriction
  14. Real-world models: Stack = undo history; Queue = task scheduling; Priority Queue = event simulation

Output:

Plaintext
=== stack (LIFO) ===
Call stack (top to bottom):
  getRate()
  calculateTax()
  main()

=== queue (FIFO) ===
Processing print queue:
  Printing: Document1.pdf
  Printing: Report.docx
  Printing: Invoice.pdf

=== priority_queue (max-heap by default) ===
Dequeuing by priority (highest first):
  50
  40
  30
  20
  10

=== priority_queue (min-heap) ===
Dequeuing by priority (lowest first):
  10
  20
  30
  50

Iterators: The Glue Between Containers and Algorithms

Iterators are objects that behave like pointers, allowing algorithms to traverse containers without knowing their internal structure.

C++
#include <iostream>
#include <vector>
#include <list>
#include <set>
#include <algorithm>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};

    // --- Forward iteration ---
    cout << "=== Forward Iteration ===" << endl;
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // --- Reverse iteration ---
    cout << "\n=== Reverse Iteration (rbegin/rend) ===" << endl;
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;

    // --- Const iterator (read-only access) ---
    cout << "\n=== Const Iterator (read-only) ===" << endl;
    for (auto cit = vec.cbegin(); cit != vec.cend(); ++cit) {
        cout << *cit << " ";
        // *cit = 99;  // ERROR: cannot assign through const iterator
    }
    cout << endl;

    // --- Iterator arithmetic (only random-access iterators) ---
    cout << "\n=== Iterator Arithmetic (vector) ===" << endl;
    auto it = vec.begin();
    cout << "First element: "  << *it << endl;
    cout << "Third element: "  << *(it + 2) << endl;
    cout << "Last element:  "  << *(vec.end() - 1) << endl;

    // --- Iterator with list (bidirectional, no arithmetic) ---
    cout << "\n=== Bidirectional Iterator (list) ===" << endl;
    list<string> words = {"apple", "banana", "cherry"};
    auto listIt = words.end();
    --listIt;  // Move backward to last element
    cout << "Last word: " << *listIt << endl;
    --listIt;
    cout << "Second-to-last word: " << *listIt << endl;

    // --- Using iterators to modify elements ---
    cout << "\n=== Modifying Elements via Iterator ===" << endl;
    for (auto it2 = vec.begin(); it2 != vec.end(); ++it2) {
        *it2 *= 2;  // Double each element
    }
    cout << "After doubling: ";
    for (int v : vec) cout << v << " ";
    cout << endl;

    return 0;
}

Step-by-step explanation:

  1. vec.begin(): Returns an iterator pointing at the first element (index 0)
  2. vec.end(): Returns an iterator pointing one past the last element — not a valid element
  3. it != vec.end(): Loop condition — stops when iterator reaches the sentinel
  4. ++it: Advances iterator to next element (equivalent to pointer arithmetic for vector)
  5. *it: Dereferences iterator — gives the element value at current position
  6. rbegin() / rend(): Reverse iterators — rbegin() starts at last, rend() is before first
  7. ++rit on reverse iterator: Moves backward through the container
  8. cbegin() / cend(): Const iterators — allow reading but prevent modification
  9. Compile-time safety: *cit = 99 won’t compile, protecting data integrity
  10. it + 2: Only valid for random-access iterators (vector, deque, array)
  11. list iterators are bidirectional: Support ++ and — but not + or –
  12. advance() alternative: Use advance(it, n) for non-random-access iterators
  13. **it2 = 2: Dereferencing and assigning through non-const iterator modifies the element
  14. Iterator categories: Input, output, forward, bidirectional, random-access — each with different capabilities

Output:

Plaintext
=== Forward Iteration ===
10 20 30 40 50

=== Reverse Iteration (rbegin/rend) ===
50 40 30 20 10

=== Const Iterator (read-only) ===
10 20 30 40 50

=== Iterator Arithmetic (vector) ===
First element: 10
Third element: 30
Last element:  50

=== Bidirectional Iterator (list) ===
Last word: cherry
Second-to-last word: banana

=== Modifying Elements via Iterator ===
After doubling: 20 40 60 80 100

STL Algorithms: Powerful Operations on Any Container

STL algorithms are generic functions that work on iterator ranges, enabling the same code to operate on any compatible container.

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <string>
using namespace std;

int main() {
    vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};

    // --- Sorting ---
    cout << "=== Sorting ===" << endl;
    vector<int> sorted = nums;
    sort(sorted.begin(), sorted.end());
    cout << "Sorted ascending: ";
    for (int n : sorted) cout << n << " ";
    cout << endl;

    sort(sorted.begin(), sorted.end(), greater<int>());
    cout << "Sorted descending: ";
    for (int n : sorted) cout << n << " ";
    cout << endl;

    // --- Searching ---
    cout << "\n=== Searching ===" << endl;
    sort(nums.begin(), nums.end());  // binary_search requires sorted input
    bool found = binary_search(nums.begin(), nums.end(), 5);
    cout << "Binary search for 5: " << (found ? "Found" : "Not found") << endl;

    auto pos = find(nums.begin(), nums.end(), 9);
    if (pos != nums.end()) {
        cout << "Linear find 9 at index: " << distance(nums.begin(), pos) << endl;
    }

    // --- Counting ---
    cout << "\n=== Counting ===" << endl;
    int countOf5 = count(nums.begin(), nums.end(), 5);
    cout << "Count of 5: " << countOf5 << endl;

    int countEven = count_if(nums.begin(), nums.end(), [](int n){ return n % 2 == 0; });
    cout << "Count of even numbers: " << countEven << endl;

    // --- Transforming ---
    cout << "\n=== Transforming ===" << endl;
    vector<int> doubled(nums.size());
    transform(nums.begin(), nums.end(), doubled.begin(), [](int n){ return n * 2; });
    cout << "Doubled values: ";
    for (int n : doubled) cout << n << " ";
    cout << endl;

    // --- Accumulating ---
    cout << "\n=== Accumulating ===" << endl;
    int sum = accumulate(nums.begin(), nums.end(), 0);
    cout << "Sum of all elements: " << sum << endl;

    int product = accumulate(nums.begin(), nums.end(), 1, multiplies<int>());
    cout << "Product of all elements: " << product << endl;

    // --- Removing and Erasing ---
    cout << "\n=== Removing Duplicates ===" << endl;
    vector<int> v2 = {1, 1, 2, 3, 3, 4, 5, 5};
    auto newEnd = unique(v2.begin(), v2.end());  // Moves duplicates to end
    v2.erase(newEnd, v2.end());                  // Erase duplicates

    cout << "After removing duplicates: ";
    for (int n : v2) cout << n << " ";
    cout << endl;

    return 0;
}

Step-by-step explanation:

  1. sort(begin, end): Sorts elements in the range in-place — O(n log n) average
  2. sorted copy: We copy nums to sorted first so original is preserved
  3. greater<int>(): A functor that reverses the comparison, producing descending order
  4. binary_search(): Requires sorted input — O(log n) search, returns bool
  5. find(): Linear search — O(n), works on any range, returns iterator to element
  6. distance(begin, pos): Calculates number of steps from begin to pos (i.e., index)
  7. count(): Counts occurrences of a specific value — O(n)
  8. count_if(): Counts elements satisfying a predicate — lambda [](int n){ return n % 2 == 0; }
  9. Lambda syntax: [](int n){ return n * 2; } — anonymous function defined inline
  10. transform(): Applies function to each element, stores result in output range
  11. Output range: doubled.begin() must point to container with enough space
  12. accumulate(): Folds range into single value starting from initial value (0)
  13. multiplies<int>(): Built-in functor for multiplication — used as custom operation
  14. unique(): Moves consecutive duplicates to end, returns iterator to new logical end
  15. erase(): Combined with unique() in “erase-remove idiom” to physically delete duplicates

Output:

Plaintext
=== Sorting ===
Sorted ascending:  1 1 2 3 3 4 5 5 6 9
Sorted descending: 9 6 5 5 4 3 3 2 1 1

=== Searching ===
Binary search for 5: Found
Linear find 9 at index: 9

=== Counting ===
Count of 5: 2
Count of even numbers: 3

=== Transforming ===
Doubled values: 2 2 4 6 6 8 10 10 12 18

=== Accumulating ===
Sum of all elements: 39
Product of all elements: 19440

=== Removing Duplicates ===
After removing duplicates: 1 2 3 4 5

Lambdas and Functors: Customizing Algorithms

STL algorithms accept callable objects—functions, functors, and lambdas—to customize their behavior.

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// --- Functor (function object) ---
struct IsLongerThan {
    int threshold;
    IsLongerThan(int t) : threshold(t) {}

    bool operator()(const string& s) const {
        return static_cast<int>(s.size()) > threshold;
    }
};

int main() {
    vector<string> words = {"apple", "fig", "banana", "kiwi", "strawberry", "plum"};

    // --- Using a plain function ---
    auto printWord = [](const string& s) {
        cout << "  " << s << endl;
    };

    cout << "=== All words ===" << endl;
    for_each(words.begin(), words.end(), printWord);

    // --- Using a lambda for custom sorting ---
    cout << "\n=== Sorted by length ===" << endl;
    vector<string> sorted = words;
    sort(sorted.begin(), sorted.end(), [](const string& a, const string& b) {
        return a.size() < b.size();  // Sort shortest first
    });
    for (const string& s : sorted) cout << "  " << s << endl;

    // --- Using a functor for filtering ---
    cout << "\n=== Words longer than 4 chars (functor) ===" << endl;
    vector<string> longWords;
    copy_if(words.begin(), words.end(),
            back_inserter(longWords),
            IsLongerThan(4));
    for (const string& s : longWords) cout << "  " << s << endl;

    // --- Lambda capturing outer variable ---
    cout << "\n=== Words containing letter 'a' (capturing lambda) ===" << endl;
    char target = 'a';
    vector<string> withA;
    copy_if(words.begin(), words.end(),
            back_inserter(withA),
            [target](const string& s) {
                return s.find(target) != string::npos;
            });
    for (const string& s : withA) cout << "  " << s << endl;

    // --- transform with lambda ---
    cout << "\n=== Uppercased words ===" << endl;
    vector<string> upper(words.size());
    transform(words.begin(), words.end(), upper.begin(), [](string s) {
        for (char& c : s) c = toupper(c);
        return s;
    });
    for (const string& s : upper) cout << "  " << s << endl;

    return 0;
}

Step-by-step explanation:

  1. IsLongerThan struct: A functor — a struct/class with operator() making it callable
  2. threshold member: Functor stores state (unlike plain functions), initialized in constructor
  3. operator(): Makes the struct callable: IsLongerThan(4)(someWord) evaluates to bool
  4. Lambda for printWord: [](const string& s){ ... } — anonymous callable object
  5. for_each(): Calls the callable on every element — replaces manual loop
  6. Lambda for sort comparator: Takes two elements, returns true if first should come before second
  7. a.size() < b.size(): Sorts by string length instead of alphabetical order
  8. copy_if(): Copies elements satisfying predicate into output range
  9. back_inserter(longWords): Output iterator that calls push_back() — avoids pre-sizing
  10. IsLongerThan(4): Creates functor with threshold=4, used as predicate
  11. [target] capture: Lambda captures the target variable by value from outer scope
  12. s.find(target): Returns index of character or string::npos if not found
  13. transform() with lambda: Applies transformation to each element, storing result
  14. toupper(c): Standard function converting character to uppercase
  15. Lambdas vs functors: Lambdas are concise for simple cases; functors allow reuse and parameterization

Output:

Plaintext
=== All words ===
  apple
  fig
  banana
  kiwi
  strawberry
  plum

=== Sorted by length ===
  fig
  kiwi
  plum
  apple
  banana
  strawberry

=== Words longer than 4 chars (functor) ===
  apple
  banana
  strawberry

=== Words containing letter 'a' (capturing lambda) ===
  apple
  banana
  strawberry

=== Uppercased words ===
  APPLE
  FIG
  BANANA
  KIWI
  STRAWBERRY
  PLUM

STL Containers Performance Comparison

ContainerAccessInsert (end)Insert (middle)SearchMemory
vectorO(1)O(1) amortizedO(n)O(n)Contiguous
listO(n)O(1)O(1)*O(n)Non-contiguous
dequeO(1)O(1)O(n)O(n)Segmented
mapO(log n)O(log n)O(log n)O(log n)Tree
unordered_mapO(1) avgO(1) avgO(1) avgO(1) avgHash table
setO(log n)O(log n)O(log n)O(log n)Tree
stack/queueO(1) top/frontO(1)N/AN/AAdapted

*list insert is O(1) only when you already have an iterator to the position

Putting It All Together: A Real-World Example

Let’s build a word frequency counter combining multiple STL components:

C++
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <sstream>
#include <string>
using namespace std;

int main() {
    string text = "the quick brown fox jumps over the lazy dog the fox";
    
    // Step 1: Tokenize text into words (vector as working storage)
    vector<string> words;
    istringstream stream(text);
    string word;
    while (stream >> word) {
        words.push_back(word);
    }
    
    cout << "=== Total words: " << words.size() << " ===" << endl;
    
    // Step 2: Count frequency (map for key-value storage)
    map<string, int> frequency;
    for (const string& w : words) {
        frequency[w]++;  // Creates entry with 0 if not exists, then increments
    }
    
    cout << "\n=== Word Frequencies (alphabetical) ===" << endl;
    for (const auto& [w, count] : frequency) {
        cout << "  " << w << ": " << count << endl;
    }
    
    // Step 3: Sort by frequency (vector of pairs + custom sort)
    vector<pair<string, int>> freqVec(frequency.begin(), frequency.end());
    sort(freqVec.begin(), freqVec.end(),
         [](const pair<string, int>& a, const pair<string, int>& b) {
             return a.second > b.second;  // Sort by count, descending
         });
    
    cout << "\n=== Top Words by Frequency ===" << endl;
    int shown = 0;
    for (const auto& [w, count] : freqVec) {
        cout << "  " << w << ": " << count << endl;
        if (++shown >= 5) break;  // Show top 5 only
    }
    
    // Step 4: Find words that appear more than once (copy_if)
    vector<string> repeatedWords;
    copy_if(freqVec.begin(), freqVec.end(),
            back_inserter(repeatedWords),
            [](const pair<string, int>& p) { return p.second > 1; });
    // Note: we stored pairs, so adjust:
    
    cout << "\n=== Words appearing more than once ===" << endl;
    for (const auto& [w, count] : freqVec) {
        if (count > 1) cout << "  " << w << " (" << count << " times)" << endl;
    }
    
    // Step 5: Total unique words (set)
    int uniqueCount = frequency.size();
    cout << "\n=== Summary ===" << endl;
    cout << "  Total words: "  << words.size() << endl;
    cout << "  Unique words: " << uniqueCount << endl;
    
    return 0;
}

Step-by-step explanation:

  1. istringstream: Wraps a string so it can be read word by word using >>
  2. push_back(word): Each extracted word added to words vector
  3. map<string, int> frequency: Key = word, value = count; automatically sorted
  4. frequency[w]++: If key doesn’t exist, map creates it with value 0, then increments
  5. Structured binding [w, count]: C++17 syntax to unpack pair in range-for
  6. Map auto-sorts: Output is alphabetical because map orders by key
  7. vector<pair<string,int>>: Copy map entries into vector to allow reordering
  8. Construct from iterators: vector(frequency.begin(), frequency.end()) copies all pairs
  9. Custom sort comparator: a.second > b.second sorts by value (count) descending
  10. Top-5 loop: Manually count shown items and break after 5
  11. Multiple STL components: vector, map, sort, copy_if, back_inserter work together
  12. frequency.size(): Number of map entries = number of unique words
  13. Composable design: Each component does one job; they compose into powerful solution
  14. Real pattern: Tokenize → aggregate → sort → filter — a very common data-processing pipeline

Output:

Plaintext
=== Total words: 10 ===

=== Word Frequencies (alphabetical) ===
  brown: 1
  dog: 1
  fox: 2
  jumps: 1
  lazy: 1
  over: 1
  quick: 1
  the: 3

=== Top Words by Frequency ===
  the: 3
  fox: 2
  brown: 1
  dog: 1
  jumps: 1

=== Words appearing more than once ===
  the (3 times)
  fox (2 times)

=== Summary ===
  Total words: 10
  Unique words: 8

Conclusion: The STL as Your Foundation

The Standard Template Library is not just a collection of utilities—it’s the backbone of modern C++ programming. By mastering its three pillars—containers, iterators, and algorithms—you gain access to decades of optimized, tested, and standardized code that handles the low-level details so you can focus on solving real problems.

Key takeaways:

  • Containers manage data storage: vector for arrays, list for linked lists, map for key-value pairs, set for unique sorted values
  • Iterators are the universal interface between containers and algorithms, behaving like smart pointers
  • Algorithms like sort, find, transform, and accumulate work on any iterator range—completely decoupled from containers
  • Container adapters (stack, queue, priority_queue) provide specialized interfaces over existing containers
  • Lambdas and functors customize algorithm behavior inline, replacing verbose loop logic
  • Choose containers wisely: vector for random access, map for fast key lookup, unordered_map for O(1) average performance
  • The erase-remove idiom (unique + erase) efficiently removes elements in-place
  • back_inserter enables growing output containers on the fly without pre-sizing

The STL’s design—separating storage from access from operations—is a masterpiece of software engineering. Once you think in terms of containers, iterators, and algorithms, you’ll find that most data manipulation problems decompose naturally into these building blocks. Use the STL, and write less code that does more—correctly, efficiently, and expressively.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Top Data Science Bootcamps Compared: Which is Right for You?

Compare top data science bootcamps including curriculum, cost, outcomes, and learning formats. Discover which bootcamp…

Vectors and Matrices Explained for Robot Movement

Learn how vectors and matrices control robot movement. Understand position, velocity, rotation, and transformations with…

The Basics of Soldering: How to Create Permanent Connections

The Basics of Soldering: How to Create Permanent Connections

Learn soldering basics from equipment selection to technique, temperature, and finishing touches to create reliable…

Exploring Capacitors: Types and Capacitance Values

Discover the different types of capacitors, their capacitance values, and applications. Learn how capacitors function…

Kindred Raises $125M for Peer-to-Peer Home Exchange Platform

Travel platform Kindred raises $125 million across Series B and C rounds for peer-to-peer home…

Understanding Transistors: The Building Blocks of Modern Electronics

Understanding Transistors: The Building Blocks of Modern Electronics

Learn what transistors are, how BJTs and MOSFETs work, why they’re the foundation of all…

Click For More
0
Would love your thoughts, please comment.x
()
x