Skip to content

Algorithms and Composition

"That's a rotate!"

Overview

Sean Parent is famous for demonstrating how standard algorithms, particularly std::rotate, can solve problems that appear to require custom loops. His talks on algorithms emphasize mastering the standard library, understanding algorithm composition, and building new algorithms from existing primitives.

Key presentations include "C++ Seasoning", "What's Your Function?", "Better Code: Algorithms - Preliminaries", and "Better Code: Algorithms - Composition".

What is an Algorithm?

Sean Parent provides a formal definition that goes beyond "a set of steps":

"An algorithm is a procedure that maps a set of inputs to a set of outputs, with a set of requirements on the inputs, a set of guarantees on the outputs, and a set of complexity guarantees."

This definition emphasizes:

  • Requirements: Pre-conditions (e.g., a range must be sorted).
  • Guarantees: Post-conditions (e.g., the range is now partitioned).
  • Complexity: Big-O performance guarantees (e.g., linear time).

The Three Goals for Better Code

In "C++ Seasoning", Parent proposed three goals to improve code quality:

  1. No Raw Loops: Encapsulate all loops into named algorithms.
  2. No Raw Synchronization Primitives: Use task-based parallelism or high-level abstractions instead of mutexes/locks.
  3. No Raw Pointers: Use value semantics or smart pointers to manage ownership.

Why Algorithms Matter

1. Express Intent

Algorithms describe what you're doing, not how:

cpp
// Loop: HOW to find
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == target) return it;
}
return v.end();

// Algorithm: WHAT you're doing
return std::find(v.begin(), v.end(), target);

2. Correctness

Standard algorithms are tested and proven:

cpp
// Loop: potential bugs
for (size_t i = 0; i <= v.size(); ++i) {  // Bug: <= instead of <
    process(v[i]);
}

// Algorithm: correct by construction
std::for_each(v.begin(), v.end(), process);

3. Performance

Algorithms can use optimized implementations:

cpp
// std::copy can use memmove for trivially copyable types
// std::sort uses introsort (quicksort + heapsort + insertion sort)
// std::find can use SIMD on some platforms

4. Local Reasoning

Raw loops often leave variables in a "messy" state (e.g., loop counters, intermediate flags) that persist after the loop ends. Algorithms encapsulate this state, ensuring that after the call, you only need to care about the result (the return value or the modified range).

"If you have a loop, you have to reason about the state of the system for every iteration of that loop... If you use an algorithm, you only have to reason about the state of the system before and after that algorithm." — Sean Parent

5. Composability

Algorithms combine naturally:

cpp
// Find the first negative number after the first positive
auto pos = std::find_if(v.begin(), v.end(), [](int x) { return x > 0; });
auto neg = std::find_if(pos, v.end(), [](int x) { return x < 0; });

Essential Algorithms

Non-Modifying Sequence Operations

AlgorithmPurposeExample
find, find_ifLocate elementfind(v.begin(), v.end(), 42)
count, count_ifCount occurrencescount_if(v.begin(), v.end(), pred)
all_of, any_of, none_ofTest predicateall_of(v.begin(), v.end(), pred)
equalCompare rangesequal(a.begin(), a.end(), b.begin())
mismatchFind first differencemismatch(a.begin(), a.end(), b.begin())

Modifying Sequence Operations

AlgorithmPurposeExample
copy, copy_ifCopy elementscopy(src.begin(), src.end(), dst.begin())
transformApply functiontransform(v.begin(), v.end(), v.begin(), f)
fillSet all to valuefill(v.begin(), v.end(), 0)
generateFill with generatorgenerate(v.begin(), v.end(), rand)
remove, remove_ifPrepare for eraseremove_if(v.begin(), v.end(), pred)
replace, replace_ifSubstitute valuesreplace(v.begin(), v.end(), old, new)
swap_rangesExchange rangesswap_ranges(a.begin(), a.end(), b.begin())
reverseReverse orderreverse(v.begin(), v.end())
rotateCycle elementsrotate(v.begin(), v.begin() + k, v.end())

Partitioning Operations

AlgorithmPurposeStability
partitionDivide by predicateUnstable
stable_partitionDivide preserving orderStable
partition_pointFind partition boundary-
is_partitionedTest if partitioned-

Sorting Operations

AlgorithmPurposeComplexity
sortSort rangeO(n log n)
stable_sortSort preserving equal orderO(n log n)
partial_sortSort first k elementsO(n log k)
nth_elementPut nth element in placeO(n)
is_sortedCheck if sortedO(n)

Binary Search (Sorted Ranges)

AlgorithmPurpose
lower_boundFirst element ≥ value
upper_boundFirst element > value
equal_rangeRange of equal elements
binary_searchCheck if present

Numeric Operations (<numeric>)

AlgorithmPurpose
accumulateFold left
reduceParallel fold
inner_productDot product
partial_sumRunning total
adjacent_differenceConsecutive differences
iotaFill with incrementing values

Basis Algorithms

A "basis" algorithm is a fundamental building block from which many other algorithms can be composed. Sean Parent identifies several key basis algorithms:

  1. std::rotate: The basis for moving ranges and reordering.
  2. std::stable_partition: The basis for gathering and filtering while preserving order.
  3. std::sort / std::nth_element: The basis for selection and ordering.

The Selection Concept

Many algorithms operate on a Selection—a subset of elements in a range that satisfy a predicate. In a UI, this might be the items a user has clicked.

  • gather collects a selection around a point.
  • slide moves a selection (expressed as a contiguous range) to a new position.
  • stable_partition divides a range into those that are selected and those that are not.

The Power of Rotate

Sean Parent's favorite algorithm, std::rotate, is surprisingly versatile:

cpp
// rotate(first, middle, last)
// Moves [middle, last) to the front
// Returns iterator to original first element in new position

std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::rotate(v.begin(), v.begin() + 2, v.end());
// v = {3, 4, 5, 1, 2}
// it points to 1

Rotate Use Cases

1. Move element to front:

cpp
auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {
    std::rotate(v.begin(), it, it + 1);
}

2. Move element to back:

cpp
auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {
    std::rotate(it, it + 1, v.end());
}

3. Insert at position (after element exists):

cpp
// Move element at 'from' to position 'to'
if (from < to) {
    std::rotate(from, from + 1, to + 1);
} else {
    std::rotate(to, from, from + 1);
}

Slide: Moving a Range

Sean Parent introduced the slide algorithm for moving a subrange:

cpp
template<typename I>  // I models RandomAccessIterator
auto slide(I first, I last, I pos) -> std::pair<I, I> {
    if (pos < first) return { pos, std::rotate(pos, first, last) };
    if (last < pos)  return { std::rotate(first, last, pos), pos };
    return { first, last };
}

Usage:

cpp
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7};
// Move [3, 4, 5] to position after 6
auto [new_first, new_last] = slide(v.begin() + 2, v.begin() + 5, v.begin() + 6);
// v = {1, 2, 6, 3, 4, 5, 7}

Gather: Collecting Elements

The gather algorithm collects elements matching a predicate around a position:

cpp
template<typename I, typename P>
auto gather(I first, I last, I pos, P pred) -> std::pair<I, I> {
    return {
        std::stable_partition(first, pos, std::not_fn(pred)),
        std::stable_partition(pos, last, pred)
    };
}

Usage:

cpp
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto is_even = [](int x) { return x % 2 == 0; };

// Gather even numbers around position 4
auto [gfirst, glast] = gather(v.begin(), v.end(), v.begin() + 4, is_even);
// v might be: {1, 3, 5, 2, 4, 6, 8, 7, 9}
//                     ^^^^^^^^^^^^ even numbers gathered

Algorithm Composition Patterns

Erase-Remove Idiom

cpp
// Remove elements matching predicate
v.erase(
    std::remove_if(v.begin(), v.end(), pred),
    v.end()
);

// C++20 version
std::erase_if(v, pred);

Transform-Accumulate

cpp
// Sum of squares
auto sum_of_squares = std::transform_reduce(
    v.begin(), v.end(),
    0,                           // Initial value
    std::plus<>{},               // Reduce operation
    [](int x) { return x * x; }  // Transform operation
);

Sort Subrange

Used to sort only the elements that are currently visible (e.g., in a UI list), which is much faster than a full sort.

cpp
template <typename I, typename O = std::less<>>
void sort_subrange(I first, I last, I sub_first, I sub_last, O order = O()) {
    if (sub_first == sub_last) return;
    if (sub_first != first) {
        std::nth_element(first, sub_first, last, order);
    }
    std::partial_sort(sub_first, sub_last, last, order);
}

Partition-Based Algorithms

stable_partition is a powerful basis for many algorithms. For example, gather is composed of two stable_partition calls.

cpp
// Move all zeros to the end
auto boundary = std::stable_partition(
    v.begin(), v.end(),
    [](int x) { return x != 0; }
);

Finding Unique Elements

cpp
// Sort and remove duplicates
std::sort(v.begin(), v.end());
v.erase(
    std::unique(v.begin(), v.end()),
    v.end()
);

Building Custom Algorithms

Traits for Algorithm Selection

cpp
template<typename I>
struct is_random_access_iterator : std::bool_constant<
    std::is_same_v<
        typename std::iterator_traits<I>::iterator_category,
        std::random_access_iterator_tag
    >
> {};

template<typename I>
void advance_impl(I& it, int n, std::random_access_iterator_tag) {
    it += n;  // O(1)
}

template<typename I>
void advance_impl(I& it, int n, std::input_iterator_tag) {
    while (n-- > 0) ++it;  // O(n)
}

Algorithm Customization Points

cpp
// Using swap customization point
template<typename I>
void my_reverse(I first, I last) {
    while (first != last && first != --last) {
        using std::swap;
        swap(*first++, *last);  // ADL finds custom swap
    }
}

Guidelines for Using Algorithms

1. Know What's Available

Before writing a loop, check if an algorithm exists:

cpp
// Don't write this loop:
bool found = false;
for (const auto& x : v) {
    if (pred(x)) { found = true; break; }
}

// Use this instead:
bool found = std::any_of(v.begin(), v.end(), pred);

2. Prefer Named Operations

If a lambda is more than a simple expression, give it a name. This makes the algorithm call read like English.

cpp
// Instead of:
auto it = std::find_if(v.begin(), v.end(), [](auto& x) {
    return x.is_active() && !x.is_hidden();
});

// Use this instead:
auto is_visible_active = [](const auto& x) {
    return x.is_active() && !x.is_hidden();
};
auto it = std::find_if(v.begin(), v.end(), is_visible_active);

3. Accumulate vs. Reduce

Sean Parent notes that std::accumulate is a "left fold" and is strictly serial. std::reduce is an actual algorithm that allows for parallel execution because it doesn't guarantee order of operations (requires associativity).

  • Use accumulate when order matters (e.g., string concatenation).
  • Use reduce (or transform_reduce) for mathematical operations where order doesn't matter.

4. Use Ranges (C++20)

cpp
// Traditional
std::sort(v.begin(), v.end());
auto it = std::find(v.begin(), v.end(), target);

// Ranges
std::ranges::sort(v);
auto it = std::ranges::find(v, target);

// With views (lazy)
auto evens = v | std::views::filter([](int x) { return x % 2 == 0; })
               | std::views::transform([](int x) { return x * 2; });

5. Compose, Don't Loop

cpp
// Instead of nested loops:
std::vector<std::pair<int, int>> pairs;
for (auto a : v1) {
    for (auto b : v2) {
        if (pred(a, b)) pairs.emplace_back(a, b);
    }
}

// Compose algorithms (with ranges):
auto pairs = std::views::cartesian_product(v1, v2)  // C++23
           | std::views::filter([](auto p) { return pred(p.first, p.second); })
           | std::ranges::to<std::vector>();

6. Understand Complexity

Choose algorithms based on complexity requirements:

cpp
// O(n) - single pass needed
std::find_if(v.begin(), v.end(), pred);

// O(n log n) - sorting acceptable
std::sort(v.begin(), v.end());

// O(log n) - requires sorted input
std::lower_bound(v.begin(), v.end(), target);

// O(1) amortized - for frequent access by key
std::unordered_map<K, V> map;

Anti-Patterns

Reimplementing Standard Algorithms

cpp
// BAD: Manual binary search
int low = 0, high = v.size() - 1;
while (low <= high) {
    int mid = low + (high - low) / 2;
    if (v[mid] == target) return mid;
    if (v[mid] < target) low = mid + 1;
    else high = mid - 1;
}
return -1;

// GOOD: Use standard algorithm
auto it = std::lower_bound(v.begin(), v.end(), target);
if (it != v.end() && *it == target) return it - v.begin();
return -1;

Using Wrong Algorithm

cpp
// BAD: Using sort when you only need nth element
std::sort(v.begin(), v.end());
auto median = v[v.size() / 2];

// GOOD: Use nth_element (O(n) vs O(n log n))
std::nth_element(v.begin(), v.begin() + v.size() / 2, v.end());
auto median = v[v.size() / 2];

References

Primary Sources

Further Reading


"If you want to improve the code quality in your organization, replace all of your coding guidelines with one goal: No Raw Loops." — Sean Parent