Lesson 7 of 12
You have been writing loops since Module 4. Most of them have a name in <algorithm>, and the named
version is shorter, harder to get wrong, and — measurably — not slower.
The ones worth memorising
#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>
int main() {
std::vector<int> v = {5, 3, 8, 1, 9, 2, 7};
// Searching
auto it = std::find(v.begin(), v.end(), 8);
std::cout << "found 8 at index " << (it - v.begin()) << "\n";
std::cout << "any > 8: " << std::boolalpha
<< std::any_of(v.begin(), v.end(), [](int x) { return x > 8; }) << "\n";
std::cout << "all > 0: "
<< std::all_of(v.begin(), v.end(), [](int x) { return x > 0; }) << "\n";
std::cout << "count of odd: "
<< std::count_if(v.begin(), v.end(), [](int x) { return x % 2 != 0; }) << "\n";
// Reductions
std::cout << "sum: " << std::accumulate(v.begin(), v.end(), 0) << "\n";
std::cout << "product: "
<< std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>{}) << "\n";
std::cout << "max: " << *std::max_element(v.begin(), v.end()) << "\n";
auto [lo, hi] = std::minmax_element(v.begin(), v.end());
std::cout << "min " << *lo << " max " << *hi << "\n";
// Transform into a new container
std::vector<int> squares(v.size());
std::transform(v.begin(), v.end(), squares.begin(), [](int x) { return x * x; });
std::cout << "squares: ";
for (int x : squares) std::cout << x << " ";
std::cout << "\n";
// Sorting
std::sort(v.begin(), v.end());
std::cout << "sorted: ";
for (int x : v) std::cout << x << " ";
std::cout << "\n";
// binary_search REQUIRES a sorted range
std::cout << "binary_search 7: " << std::binary_search(v.begin(), v.end(), 7) << "\n";
// Copy only what matches
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
[](int x) { return x % 2 == 0; });
std::cout << "evens: ";
for (int x : evens) std::cout << x << " ";
std::cout << "\n";
return 0;
}
found 8 at index 2
any > 8: true
all > 0: true
count of odd: 5
sum: 35
product: 15120
max: 9
min 1 max 9
squares: 25 9 64 1 81 4 49
sorted: 1 2 3 5 7 8 9
binary_search 7: true
evens: 2 8
Two details in there that are easy to miss.
std::accumulate's initial value fixes the result type. std::accumulate(v.begin(), v.end(), 0)
accumulates into an int. On a std::vector<double> that silently truncates every addition — the
initial value must be 0.0. This is a real bug that produces plausible-looking wrong numbers.
std::back_inserter is why copy_if can write into an empty vector. An algorithm cannot resize
a container (lesson 13.3), so back_inserter wraps the container in an output iterator whose
operator= calls push_back. Without it you must size the destination first.
The catalogue
Roughly organised by what they do:
| Group | Members |
|---|---|
| Find | find, find_if, find_first_of, search, adjacent_find |
| Test | all_of, any_of, none_of, equal, count, count_if |
| Copy | copy, copy_if, copy_n, move, swap_ranges |
| Modify | transform, fill, generate, replace, replace_if, reverse, rotate |
| Remove | remove, remove_if, unique (and C++20 erase, erase_if) |
| Sort | sort, stable_sort, partial_sort, nth_element, is_sorted |
| Sorted-range | binary_search, lower_bound, upper_bound, merge, includes |
| Set ops | set_union, set_intersection, set_difference |
| Heap | make_heap, push_heap, pop_heap, sort_heap |
| Numeric (<numeric>) | accumulate, inner_product, iota, partial_sum, reduce |
| Min/max | min, max, minmax, min_element, max_element, clamp |
You do not need to memorise the list. You need to have read it once, so that when you are about to write a loop, you recognise that it has a name.
Are they slower than a hand-written loop?
#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
template <typename F>
long long time_us(F f) {
auto t0 = std::chrono::steady_clock::now();
f();
auto t1 = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
}
int main() {
const int N = 2000000;
std::vector<int> data(N);
std::mt19937 gen(42);
for (int& x : data) x = static_cast<int>(gen());
auto a = data;
auto b = data;
long long sink = 0;
// Hand-written loop vs std::count_if: the algorithm is not slower.
auto hand = time_us([&] {
int c = 0;
for (int x : a) if (x % 3 == 0) ++c;
sink += c;
});
auto algo = time_us([&] {
sink += std::count_if(b.begin(), b.end(), [](int x) { return x % 3 == 0; });
});
std::cout << "hand-written loop: " << hand << " us\n";
std::cout << "std::count_if: " << algo << " us\n";
std::cout << "(sink " << sink << ")\n";
// sort vs partial_sort vs nth_element for "the 10 smallest"
auto c1 = data, c2 = data, c3 = data;
auto t_sort = time_us([&]{ std::sort(c1.begin(), c1.end()); });
auto t_partial = time_us([&]{ std::partial_sort(c2.begin(), c2.begin()+10, c2.end()); });
auto t_nth = time_us([&]{ std::nth_element(c3.begin(), c3.begin()+10, c3.end()); });
std::cout << "\nfinding the 10 smallest of " << N << ":\n";
std::cout << " std::sort: " << t_sort << " us\n";
std::cout << " std::partial_sort: " << t_partial << " us\n";
std::cout << " std::nth_element: " << t_nth << " us\n";
std::cout << " first 3 agree: " << std::boolalpha
<< (c1[0]==c2[0] && c1[1]==c2[1] && c1[2]==c2[2]) << "\n";
return 0;
}
A representative run, compiled -O2:
hand-written loop: 1893 us
std::count_if: 1742 us
(sink 1333390)
finding the 10 smallest of 2000000:
std::sort: 160291 us
std::partial_sort: 1529 us
std::nth_element: 14720 us
first 3 agree: true
No, they are not slower. std::count_if came out marginally faster than the equivalent loop —
close enough that the difference is noise, which is the point. The lambda is inlined and what remains
is the same machine code. There is no abstraction penalty to pay.
The second block is the interesting one, and it contains a surprise.
std::sort was 105× slower than std::partial_sort for finding the 10 smallest of two million,
because it sorted 1,999,990 elements nobody asked about. Using the right algorithm mattered far
more than any micro-optimisation could.
But std::nth_element was 9× slower than std::partial_sort, despite being O(n) against
O(n log k). That inverts the complexity ranking, and it held across repeated runs.
The reason is what each actually does. partial_sort with k=10 makes one pass over the data
maintaining a 10-element heap — the working set is 40 bytes, it stays in L1, and it writes almost
nothing. nth_element runs quickselect, partitioning two million elements repeatedly, writing across
the whole array on every pass. Big-O counts comparisons; the machine charges for memory traffic.
The honest conclusion is not "nth_element is bad". It is that for small k, partial_sort wins,
and that complexity classes predict the shape of a curve rather than which of two implementations is
faster at your n. Measure at the size you actually have.
Predicates and lambdas
Any algorithm ending in _if takes a predicate — anything callable returning bool:
// std::count_if(v.begin(), v.end(), [](int x) { return x > 5; }); // lambda
// std::count_if(v.begin(), v.end(), is_positive); // function
// std::count_if(v.begin(), v.end(), IsAbove{5}); // function object
Module 14 covers lambdas properly. For now: [](int x) { return x > 5; } is an unnamed function
written at the call site, and it is what makes the algorithms usable.
The sorted-range trap
Six algorithms require a sorted range and give wrong answers on an unsorted one, silently:
// std::binary_search, std::lower_bound, std::upper_bound,
// std::equal_range, std::merge, std::includes
Nothing checks. std::binary_search on unsorted data returns whatever the bisection happens to
land on, and there is no diagnostic. If you are not certain the range is sorted, assert it:
// assert(std::is_sorted(v.begin(), v.end()));
std::is_sorted is O(n), so the assert costs as much as a linear search would — which is fine in a
debug build and compiled away with -DNDEBUG in release.
sort versus stable_sort
std::sort may reorder elements it considers equal. std::stable_sort will not.
That matters when sorting by one field and expecting the previous order to survive within groups —
sorting employees by department and expecting each department still alphabetical. std::sort gives
no such guarantee; std::stable_sort does, at the cost of extra memory.
Try it
Take a std::vector<std::string> of names. Using only algorithms, no raw loops: count names
longer than 5 characters, produce a vector of their lengths, sort by length then alphabetically,
find the first name starting with "M", and remove all names containing "z".
Checkpoint
- Most loops you write have a name in
<algorithm>; the named version is clearer and not slower. std::count_ifmeasured marginally faster than the equivalent hand-written loop.std::accumulate's initial value fixes the result type — use0.0for doubles.std::back_inserterlets an algorithm grow a container it otherwise could not resize.- Choosing the right algorithm beat micro-optimisation by 105× (
sortvspartial_sort). - For small k,
partial_sortbeatnth_element9× despite worse complexity — measure at your n. - Six algorithms silently require a sorted range; assert with
std::is_sortedif unsure. std::sortmay reorder equal elements;std::stable_sortwill not.- Algorithms ending in
_iftake a predicate — usually a lambda (Module 14).
Questions about this lesson
Log in to ask a question.