find, sort, accumulate, transform. Over 100 algorithms that work on any container through iterators — plus lambdas, the most powerful tool in modern C++.
You have a vector<string> of words. You want to sort them, remove duplicates, and count how many are longer than five characters. You could write three nested loops. Or you could write three lines:
c++ sort(words.begin(), words.end()); auto last = unique(words.begin(), words.end()); words.erase(last, words.end()); auto n = count_if(words.begin(), words.end(), [](const string &s){ return s.size() > 5; });
The standard library provides over 100 generic algorithms. They are "generic" because they work on any container type — vector, list, deque, even built-in arrays — through iterators. They don't know or care what container holds the data. They just need iterators to traverse it.
Click a category to see example algorithms. All operate through iterators, never touching the container directly.
Read-only algorithms examine elements without modifying them. They are the safest algorithms — you can't corrupt your data by calling them.
| Algorithm | Header | What it does |
|---|---|---|
find(beg, end, val) | algorithm | Returns iterator to first element equal to val, or end |
count(beg, end, val) | algorithm | Returns number of elements equal to val |
accumulate(beg, end, init) | numeric | Returns sum of elements starting from init |
equal(beg1, end1, beg2) | algorithm | Returns true if two ranges are element-wise equal |
find_if(beg, end, pred) | algorithm | Returns iterator to first element for which pred is true |
count_if(beg, end, pred) | algorithm | Counts elements for which pred is true |
c++ vector<int> v = {3, 1, 4, 1, 5, 9}; // find returns an iterator auto it = find(v.cbegin(), v.cend(), 5); // points to 5 // accumulate sums — third arg sets type AND initial value int total = accumulate(v.cbegin(), v.cend(), 0); // 23 // count how many 1s auto n = count(v.cbegin(), v.cend(), 1); // 2
vector<double> but pass 0 (an int) as the initial value, the result is truncated to int. Pass 0.0 to get a double result. The type of the third argument determines the return type.accumulate(v.cbegin(), v.cend(), 0) on a vector<double>?Some algorithms assign new values to elements. Others rearrange element order. Both categories share a critical rule: the destination must have enough room.
| Algorithm | What it does |
|---|---|
fill(beg, end, val) | Assigns val to every element in range |
fill_n(dest, n, val) | Assigns val to n elements starting at dest |
copy(beg, end, dest) | Copies elements to dest; returns past-the-end of dest |
replace(beg, end, old, new) | Replaces all old values with new |
transform(beg, end, dest, fn) | Applies fn to each element, writes result to dest |
fill_n(dest, n, val) assumes dest points to a range with at least n elements. If it doesn't, you get undefined behavior. This is the most common algorithm bug.A back_inserter creates a special iterator that calls push_back on the container each time you assign to it. This lets write algorithms grow the container as needed:
c++ vector<int> v; // empty // WRONG: fill_n writes to 10 nonexistent elements // fill_n(v.begin(), 10, 0); // undefined behavior! // RIGHT: back_inserter creates elements via push_back fill_n(back_inserter(v), 10, 0); // v now has 10 zeros
sort arranges elements in ascending order using <. unique rearranges so adjacent duplicates are moved to the end, returning an iterator to the first "garbage" element:
c++ vector<string> words = {"the", "fox", "the", "red", "fox"}; sort(words.begin(), words.end()); // words: {"fox","fox","red","the","the"} auto end_unique = unique(words.begin(), words.end()); // words: {"fox","red","the", ?, ?} — last two are garbage words.erase(end_unique, words.end()); // words: {"fox","red","the"}
erase yourself to actually shrink the container. This is because algorithms can't perform container operations — only the container itself can add or remove elements.erase after unique to actually remove duplicates?Many algorithms take a predicate — a callable that returns a value convertible to bool. Predicates let you customize what an algorithm does. A unary predicate takes one argument; a binary predicate takes two.
c++ // sort by length using a binary predicate sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); });
That [](...){...} syntax is a lambda expression — an anonymous, inline function. Lambdas are the most important feature added to C++11 for working with algorithms.
c++ // A lambda that tests if a string is longer than a given size auto isLong = [](const string &s) { return s.size() > 5; }; // Use with find_if auto it = find_if(words.begin(), words.end(), isLong); // it points to the first word longer than 5 characters // Use with count_if auto n = count_if(words.begin(), words.end(), isLong); // n = number of words longer than 5 characters
The for_each algorithm applies a callable to every element in a range. It's useful for performing side effects like printing:
c++ for_each(words.begin(), words.end(), [](const string &s) { cout << s << " "; }); cout << endl;
[] in a lambda expression specify?A lambda can only use variables from its enclosing scope if it captures them. The capture list specifies which variables and how:
| Capture | Meaning |
|---|---|
[] | Capture nothing |
[sz] | Capture sz by value (copy) |
[&sz] | Capture sz by reference |
[=] | Capture all used variables by value |
[&] | Capture all used variables by reference |
[=, &os] | All by value except os by reference |
[&, sz] | All by reference except sz by value |
c++ size_t sz = 5; // Capture sz by value — lambda gets a COPY of sz auto wc = find_if(words.begin(), words.end(), [sz](const string &s) { return s.size() >= sz; }); // Capture by reference — lambda uses the actual variable ostream &os = cout; for_each(words.begin(), words.end(), [&os](const string &s) { os << s << " "; });
See how value captures freeze a copy while reference captures track the original. Click "Change x" to modify the captured variable.
By default, a lambda cannot modify its value-captured variables. Adding mutable lifts this restriction:
c++ int v = 42; auto f = [v]() mutable { return ++v; }; v = 0; auto j = f(); // j is 43 (lambda's copy was 42, incremented to 43)
Lambdas are great for one-off predicates. But what if you have an existing function that takes extra arguments? The bind function (from <functional>) lets you adapt it:
c++ #include <functional> using namespace std::placeholders; // A function that takes two args bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; } // bind fixes the second argument to 6 auto check6 = bind(check_size, _1, 6); // check6(s) calls check_size(s, 6) auto it = find_if(words.begin(), words.end(), check6);
Placeholders (_1, _2, ...) mark which arguments the bound function receives from its caller. _1 means "use the first argument passed to the bound callable."
c++ // sort descending: swap argument order sort(v.begin(), v.end(), bind(less<int>(), _2, _1)); // _2 is the second arg of the comparator, _1 the first // so less compares (b, a) instead of (a, b) → descending
By default, bind copies its non-placeholder arguments. To bind a reference, wrap it with ref() or cref():
c++ ostream &print(ostream &os, const string &s, char c) { return os << s << c; } // ref(os) passes os by reference, not copy for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
_1 represent in a bind expression?Different algorithms need different iterator capabilities. The standard defines five iterator categories, forming a hierarchy from weakest to strongest:
| Category | Operations | Example Containers |
|---|---|---|
| Input | Read once, advance: ==, !=, ++, * | istream_iterator |
| Output | Write once, advance: ++, * | ostream_iterator, back_inserter |
| Forward | Read/write, multi-pass: Input + can revisit | forward_list |
| Bidirectional | Forward + -- | list, set, map |
| Random Access | Bidirectional + +, -, [], < | vector, deque, string, array |
sort requires random-access iterators (it needs to jump around). You cannot sort a list with std::sort — use list::sort() instead. find only needs input iterators, so it works on anything.Three special iterators that turn writes into container insertions:
| Inserter | Calls | Use case |
|---|---|---|
back_inserter(c) | c.push_back() | Append to vector, string, deque |
front_inserter(c) | c.push_front() | Prepend to list, deque |
inserter(c, iter) | c.insert(iter, ...) | Insert before iter |
Reverse iterators traverse backward. rbegin() points to the last element; rend() points before the first. ++ on a reverse iterator moves backward:
c++ // Find last occurrence of a comma auto rcomma = find(line.crbegin(), line.crend(), ','); // Convert reverse iterator to forward: .base() cout << string(rcomma.base(), line.cend());
std::sort on a list?Real-world code chains algorithms together. Let's build the classic "sort, unique, filter" pipeline step by step and watch each algorithm transform the data.
Start with a list of words. Apply algorithms one at a time and watch the data transform. Each step uses the output of the previous.
Some containers define their own versions of algorithms. list and forward_list have member versions of sort, merge, remove, reverse, and unique. These are more efficient because they can rewire pointers instead of copying elements:
c++ list<int> lst = {4, 2, 5, 1, 3}; lst.sort(); // member sort — rewires nodes, no copies lst.unique(); // member unique — actually erases nodes lst.reverse(); // member reverse — rewires prev/next pointers
std::sort won't even compile for list (it needs random-access iterators). But even for operations that compile, like std::unique, the member version is better because it actually removes elements rather than just rearranging them.Generic algorithms are the bridge between containers and custom logic. With lambdas and iterators, you can express complex data transformations in a few clean lines.
| This Chapter | Builds Toward |
|---|---|
| Lambdas and captures | Function objects and std::function (Ch 14) |
| Iterator categories | Template concepts and constraints (Ch 16) |
| Algorithm pipelines | Ranges library in C++20 |
| Predicates and callables | Custom comparators for associative containers (Ch 11) |
| back_inserter | Iterator adaptors and allocators (Ch 12) |
Next up: Chapter 12: Dynamic Memory — smart pointers, RAII, and the art of never leaking memory.