Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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_if measured marginally faster than the equivalent hand-written loop.
  • std::accumulate's initial value fixes the result type — use 0.0 for doubles.
  • std::back_inserter lets an algorithm grow a container it otherwise could not resize.
  • Choosing the right algorithm beat micro-optimisation by 105× (sort vs partial_sort).
  • For small k, partial_sort beat nth_element 9× despite worse complexity — measure at your n.
  • Six algorithms silently require a sorted range; assert with std::is_sorted if unsure.
  • std::sort may reorder equal elements; std::stable_sort will not.
  • Algorithms ending in _if take a predicate — usually a lambda (Module 14).

Questions about this lesson

Log in to ask a question.