C++ Primer, Chapter 10

Generic Algorithms

find, sort, accumulate, transform. Over 100 algorithms that work on any container through iterators — plus lambdas, the most powerful tool in modern C++.

Prerequisites: Chapter 9 (Sequential Containers) + iterators.
9
Chapters
4+
Simulations

Chapter 0: Why Generic Algorithms?

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.

The core insight: Algorithms never execute container operations. They work entirely through iterators. An algorithm can read, write, and reorder elements, but it cannot add or remove elements from the underlying container.
Algorithm Categories

Click a category to see example algorithms. All operate through iterators, never touching the container directly.

Why are the standard library algorithms called "generic"?

Chapter 1: Read-Only Algorithms

Read-only algorithms examine elements without modifying them. They are the safest algorithms — you can't corrupt your data by calling them.

AlgorithmHeaderWhat it does
find(beg, end, val)algorithmReturns iterator to first element equal to val, or end
count(beg, end, val)algorithmReturns number of elements equal to val
accumulate(beg, end, init)numericReturns sum of elements starting from init
equal(beg1, end1, beg2)algorithmReturns true if two ranges are element-wise equal
find_if(beg, end, pred)algorithmReturns iterator to first element for which pred is true
count_if(beg, end, pred)algorithmCounts 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
The third argument to accumulate matters. If you have a 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.
Use cbegin/cend for read-only algorithms. Since these algorithms don't modify elements, passing const iterators is both safer and clearer about intent. Only pass begin/end if you need the returned iterator to modify elements.
What happens if you call accumulate(v.cbegin(), v.cend(), 0) on a vector<double>?

Chapter 2: Write & Reorder Algorithms

Some algorithms assign new values to elements. Others rearrange element order. Both categories share a critical rule: the destination must have enough room.

Algorithms That Write

AlgorithmWhat 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
Danger: algorithms do not check write operations. 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.

back_inserter: The Safety Net

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

Algorithms That Reorder

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"}
unique doesn't remove elements. It only rearranges them so duplicates are at the end. You must call 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.
Why must you call erase after unique to actually remove duplicates?

Chapter 3: Lambda Expressions

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.

Anatomy of a Lambda

[capture list]
Which local variables to use
(parameter list)
Arguments (like any function)
-> return type
Optional; deduced if body is single return
{ function body }
The code to execute
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
Lambdas vs named functions: You can pass a regular function as a predicate too. Lambdas shine when the logic is short and specific to one call site. They also have a superpower named functions lack: captures, which we'll see next.

for_each

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;
What does the capture list [] in a lambda expression specify?

Chapter 4: Lambda Captures

A lambda can only use variables from its enclosing scope if it captures them. The capture list specifies which variables and how:

CaptureMeaning
[]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 << " "; });
Value vs reference captures: A value capture copies the variable at the point the lambda is created. A reference capture uses the variable directly — if the variable changes later, the lambda sees the change. But beware: if the lambda outlives the variable it captures by reference, you get a dangling reference.
Capture by Value vs Reference

See how value captures freeze a copy while reference captures track the original. Click "Change x" to modify the captured variable.

x = 10

Mutable Lambdas

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)
If a lambda captures a variable by value and the original changes afterward, what does the lambda see?

Chapter 5: bind — The Adaptor

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

Reordering Arguments

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
bind vs lambdas: In modern C++, lambdas have largely replaced bind. Lambdas are clearer, faster (the compiler can inline them), and more flexible. Use bind when you need to adapt an existing function without rewriting it, but prefer lambdas for new code.

Binding Reference Arguments

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, ' '));
What does _1 represent in a bind expression?

Chapter 6: Iterator Categories

Different algorithms need different iterator capabilities. The standard defines five iterator categories, forming a hierarchy from weakest to strongest:

CategoryOperationsExample Containers
InputRead once, advance: ==, !=, ++, *istream_iterator
OutputWrite once, advance: ++, *ostream_iterator, back_inserter
ForwardRead/write, multi-pass: Input + can revisitforward_list
BidirectionalForward + --list, set, map
Random AccessBidirectional + +, -, [], <vector, deque, string, array
Why this matters: 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.

Insert Iterators

Three special iterators that turn writes into container insertions:

InserterCallsUse 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

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());
Why can't you use std::sort on a list?

Chapter 7: Algorithm Pipeline Simulator

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.

Algorithm Pipeline

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.

Step 0: raw data

Container-Specific Algorithms

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
Prefer member versions for list. The generic 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.

Chapter 8: Beyond — Connections

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 ChapterBuilds Toward
Lambdas and capturesFunction objects and std::function (Ch 14)
Iterator categoriesTemplate concepts and constraints (Ch 16)
Algorithm pipelinesRanges library in C++20
Predicates and callablesCustom comparators for associative containers (Ch 11)
back_inserterIterator adaptors and allocators (Ch 12)
Lippman's advice: "Using well-known library algorithms is more likely to be correct and more likely to be efficient than is a locally designed operation." Resist the urge to write raw loops when an algorithm exists. The algorithm name communicates intent.

Continue Reading

Next up: Chapter 12: Dynamic Memory — smart pointers, RAII, and the art of never leaking memory.