Skip to content
C++
Language
since C++11
Basic

Master STL Algorithms: Write Less, Do More

Apply STL algorithms to replace manual loops with expressive, bug-resistant operations on any container in your C++ code.

By the end of this page, you will know how to search, sort, transform, and fold data using the C++ standard library's algorithm functions β€” and you will understand why each one exists, what it returns, and how to avoid the mistakes that trip up nearly every developer the first time.

What and Why

The C++ standard library ships with roughly 100 algorithm functions spread across <algorithm> and <numeric>. Each one operates on a range β€” a pair of iterators that mark the beginning and one-past-the-end of some sequence of elements. Because they work through iterators, they are container-agnostic: the same std::sort call works on a vector, an array, or a deque.

Three reasons to reach for them instead of writing a raw loop:

  • Safety. The algorithm handles edge cases (empty range, single element) correctly and consistently.
  • Expressiveness. std::sort(v.begin(), v.end()) announces intent in a way a four-line loop never can.
  • Performance. Standard library implementations are heavily optimized. Under C++17, many algorithms accept an execution policy that enables automatic parallelism.

All examples on this page require C++11 for lambda support. Compile with -std=c++11 or later.

Step by Step

Searching: std::find and std::find_if

The simplest algorithm: locate an element and hand back an iterator to it.

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};

    // Search by value
    auto it = std::find(nums.begin(), nums.end(), 5);
    if (it != nums.end()) {
        std::cout << "Found 5 at index "
                  << std::distance(nums.begin(), it) << '\n';
    }

    // Search by predicate β€” first value greater than 4
    auto big = std::find_if(nums.begin(), nums.end(),
                            [](int x) { return x > 4; });
    if (big != nums.end()) {
        std::cout << "First value > 4: " << *big << '\n';
    }
}

Every algorithm returns something meaningful. std::find returns an iterator to the matching element, or .end() when nothing matches. Always compare against .end() before dereferencing.

Sorting: std::sort

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};

    std::sort(nums.begin(), nums.end()); // ascending

    for (int n : nums) std::cout << n << ' '; // 1 1 2 3 4 5 6 9
    std::cout << '\n';

    // Supply a comparator for descending order
    std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });

    for (int n : nums) std::cout << n << ' '; // 9 6 5 4 3 2 1 1
    std::cout << '\n';
}

Transforming: std::transform

std::transform applies a function to each element and writes results into an output range. The output can be the same range (in-place) or a separate container β€” but you must ensure the destination has enough space before writing.

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    std::vector<int> squares(nums.size()); // pre-size the destination

    std::transform(nums.begin(), nums.end(), squares.begin(),
                   [](int x) { return x * x; });

    for (int s : squares) std::cout << s << ' '; // 1 4 9 16 25
    std::cout << '\n';
}

Folding: std::accumulate

std::accumulate collapses a range into a single value by repeatedly applying a binary operation. The third argument is the initial value β€” it sets the starting point for the fold.

cpp
#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};

    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    std::cout << "Sum: " << sum << '\n'; // 15

    // Custom operation: product. Initial value must be 1, not 0.
    int product = std::accumulate(nums.begin(), nums.end(), 1,
                                  [](int acc, int x) { return acc * x; });
    std::cout << "Product: " << product << '\n'; // 120
}

Checking conditions across a range: std::all_of, any_of, none_of

These three algorithms apply a predicate to every element and return a bool. They short-circuit β€” they stop as soon as the answer is known, making them efficient on large ranges.

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {2, 4, 6, 8};

    auto isEven = [](int x) { return x % 2 == 0; };

    // true only if every element satisfies the predicate
    std::cout << std::all_of(nums.begin(), nums.end(), isEven) << '\n';  // 1

    std::vector<int> mixed = {1, 3, 5, 7, 8};

    // true if at least one element satisfies the predicate
    std::cout << std::any_of(mixed.begin(), mixed.end(), isEven) << '\n'; // 1

    std::vector<int> odds = {1, 3, 5, 7};

    // true if no element satisfies the predicate
    std::cout << std::none_of(odds.begin(), odds.end(), isEven) << '\n';  // 1
}

Edge-case rules (vacuous truth):

  • all_of on an empty range returns true.
  • any_of on an empty range returns false.
  • none_of on an empty range returns true.

Prefer these over count_if(...) == 0 or count_if(...) > 0 β€” they communicate intent more directly and exit early.

Common Patterns

Filter and erase: the erase-remove idiom

Removing elements that satisfy a condition is a two-step dance. std::remove_if shuffles matching elements to the back and returns an iterator to the new logical end. Then .erase() trims the container.

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};

    nums.erase(
        std::remove_if(nums.begin(), nums.end(),
                       [](int x) { return x % 2 == 0; }),
        nums.end());

    for (int n : nums) std::cout << n << ' '; // 1 3 5 7
    std::cout << '\n';
}

Transform then accumulate

A productive pipeline: derive a scalar metric from a collection of objects without an intermediate container.

cpp
#include <numeric>
#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<std::string> words = {"hello", "world", "cpp"};

    int total = std::accumulate(words.begin(), words.end(), 0,
        [](int acc, const std::string& w) {
            return acc + static_cast<int>(w.size());
        });

    std::cout << "Total chars: " << total << '\n'; // 13
}

Count with a predicate: std::count_if

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    int evens = std::count_if(nums.begin(), nums.end(),
                              [](int x) { return x % 2 == 0; });

    std::cout << evens << " even numbers\n"; // 5 even numbers
}

What Can Go Wrong

Forgetting .erase() after std::remove_if

std::remove_if does not resize the container β€” it only knows about iterators, not the container itself. The elements past the returned iterator are left in a valid-but-unspecified state, and .size() remains unchanged.

cpp
// WRONG β€” discards the return value; nothing is actually removed
std::remove_if(nums.begin(), nums.end(), pred);

// CORRECT β€” erase the tail the algorithm left behind
nums.erase(std::remove_if(nums.begin(), nums.end(), pred), nums.end());

Wrong initial value in std::accumulate

cpp
std::vector<int> nums = {2, 3, 4};

// WRONG β€” initial value 0 means the product is always 0
int bad = std::accumulate(nums.begin(), nums.end(), 0,
                          [](int a, int b) { return a * b; });

// CORRECT β€” 1 is the identity element for multiplication
int good = std::accumulate(nums.begin(), nums.end(), 1,
                           [](int a, int b) { return a * b; });

Passing an unsorted range to a binary search algorithm

std::binary_search, std::lower_bound, and std::upper_bound require the range to already be sorted. An unsorted range produces undefined behavior β€” no crash, just silently wrong results.

cpp
std::vector<int> v = {5, 3, 1, 4, 2};

// WRONG β€” undefined behavior; range is not sorted
bool found = std::binary_search(v.begin(), v.end(), 3);

// CORRECT β€” sort first
std::sort(v.begin(), v.end());
bool found_ok = std::binary_search(v.begin(), v.end(), 3);

Iterator invalidation

Never use an iterator after an operation that could reallocate the container:

cpp
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // may reallocate β€” 'it' is now dangling
std::cout << *it; // undefined behavior

Store indices rather than iterators when you need to reference elements across a potential reallocation.

Quick Reference

AlgorithmHeaderWhat it does
std::find(b, e, val)<algorithm>Iterator to first element equal to val
std::find_if(b, e, pred)<algorithm>Iterator to first element matching predicate
std::count_if(b, e, pred)<algorithm>Count of elements matching predicate
std::all_of(b, e, pred)<algorithm>true if every element matches predicate (short-circuits)
std::any_of(b, e, pred)<algorithm>true if at least one element matches predicate (short-circuits)
std::none_of(b, e, pred)<algorithm>true if no element matches predicate (short-circuits)
std::sort(b, e)<algorithm>Sort range ascending; optional comparator
std::stable_sort(b, e)<algorithm>Sort, preserving order of equal elements
std::transform(b, e, out, fn)<algorithm>Apply fn element-wise, write to out
std::remove_if(b, e, pred)<algorithm>Shuffle matches to end; return new logical end
std::copy_if(b, e, out, pred)<algorithm>Copy matching elements to out
std::accumulate(b, e, init)<numeric>Fold with + or custom binary op
std::binary_search(b, e, val)<algorithm>true if val exists in sorted range
std::min_element(b, e)<algorithm>Iterator to smallest element
std::max_element(b, e)<algorithm>Iterator to largest element

Constrained Algorithms vs Legacy Algorithms

The std::ranges:: versions of algorithms in C++20 are strictly safer than their <algorithm> counterparts because they accept a single range object instead of two iterators, eliminating an entire class of bugs. Passing mismatched begin/end iterators β€” from different containers, or in the wrong order β€” is undefined behavior with the legacy API. The ranges API makes that impossible.

cpp
#include <algorithm>
#include <ranges>
#include <vector>

std::vector<int> v = {3, 1, 4, 1, 5, 9};

// Legacy: two iterators β€” possible to pass mismatched iterators from different containers
std::sort(v.begin(), v.end());

// Ranges: takes the whole range β€” type system prevents mismatched iterators
std::ranges::sort(v);

// Ranges also supports sentinels (end may be a different type than begin)
// and constrained element types via Concepts β€” the error messages are far better

The ranges versions also require that the element type satisfies concept constraints (e.g., std::sortable). If your type does not meet the requirements, you get a readable concept-violation error pointing at the constraint, rather than a wall of template instantiation noise.

Projection Support in C++20 Ranges

Projections let you sort, search, or compare on a derived attribute without writing a comparator lambda. Pass a pointer-to-member or any callable as the fourth argument (the projection) and the algorithm applies it to each element before comparing.

cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Charlie", 30}, {"Alice", 25}, {"Bob", 35}
    };

    // Sort by name β€” no lambda needed
    std::ranges::sort(people, {}, &Person::name);
    for (auto& p : people)
        std::cout << p.name << '\n';  // Alice, Bob, Charlie

    // Sort by age descending β€” comparator + projection
    std::ranges::sort(people, std::greater{}, &Person::age);
    for (auto& p : people)
        std::cout << p.name << " " << p.age << '\n';  // Bob 35, Charlie 30, Alice 25

    // Find the youngest
    auto it = std::ranges::min_element(people, {}, &Person::age);
    std::cout << "Youngest: " << it->name << '\n';  // Alice
}

The {} in place of the comparator means "use the default comparator" (std::ranges::less). Projections compose with every constrained algorithm: find, count, min_element, max_element, unique, partition, and more.

std::views Lazy Pipelines

std::views (C++20) lets you compose filter, transform, and take operations into a lazy pipeline that allocates no intermediate storage β€” elements flow through the pipeline one at a time on demand. The pipe | operator chains adapters left to right.

cpp
#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Keep evens, square them, take the first three results
    auto result = nums
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(3);

    for (int n : result)
        std::cout << n << ' ';  // 4 16 36
    std::cout << '\n';
}

No intermediate vectors are created. Each call to the range-for loop's increment operator pulls one element through the entire pipeline. This makes it cheap to describe a complex computation over a large sequence without materialising intermediate results.

Materialise the pipeline into a container with std::ranges::to<std::vector>() (C++23), or manually via std::ranges::copy:

cpp
std::vector<int> squares;
std::ranges::copy(result, std::back_inserter(squares));

Writing Your Own Algorithm

An algorithm that works on iterator pairs and accepts a predicate follows the same contract as the standard library and composes naturally with any container. In C++20, concept constraints replace ad-hoc SFINAE and produce readable error messages.

cpp
#include <iterator>
#include <concepts>

// C++20: constrained template β€” only compiles for input iterators
// whose element type is equality-comparable to T
template <std::input_iterator It, std::sentinel_for<It> Sent, typename T>
    requires std::equality_comparable_with<std::iter_value_t<It>, T>
It find_first(It first, Sent last, const T& value) {
    while (first != last) {
        if (*first == value)
            return first;
        ++first;
    }
    return first;
}

// Projection variant: apply proj before comparing
template <std::input_iterator It, std::sentinel_for<It> Sent,
          typename T, typename Proj = std::identity>
It find_first_proj(It first, Sent last, const T& value, Proj proj = {}) {
    while (first != last) {
        if (proj(*first) == value)
            return first;
        ++first;
    }
    return first;
}

The key design decisions: return the end iterator on failure (never a raw pointer or a sentinel value), accept a projection for flexibility, and name the template parameters to match concept requirements so error messages are self-explanatory.

Algorithm Complexity Guarantees

The C++ standard mandates worst-case complexity for most algorithms β€” not average-case β€” so you know what you are committing to when you choose one. These are the guarantees the standard specifies, not implementation promises:

AlgorithmComplexityNotes
std::sortO(n log n)Introsort in practice; standard says O(n log n) worst case since C++11
std::stable_sortO(n logΒ² n) or O(n log n)O(n log n) if extra memory available
std::partial_sortO(n log k)k = distance to middle iterator
std::nth_elementO(n) averageLinear on average; O(nΒ²) worst case allowed
std::find / find_ifO(n)Linear scan, no preconditions
std::binary_searchO(log n)Requires sorted range
std::lower_boundO(log n)Requires sorted range
std::mergeO(n + m)Linear in total input size
std::set_intersectionO(n + m)Linear in total input size
std::uniqueO(n)Collapses adjacent duplicates
std::count_ifO(n)Full scan

std::nth_element is worth highlighting: it partially sorts such that the nth element is the one that would be there in a fully sorted range, and all elements before it are ≀ it, all after are β‰₯ it. It is O(n) on average β€” useful for finding medians or percentile thresholds without a full sort.

std::sample and std::shuffle

std::sample (C++17) draws a fixed-size random subset from a range without replacement, and std::shuffle randomly reorders a range in-place β€” both require a uniform random number generator. Pass a std::mt19937 seeded from std::random_device for quality randomness.

cpp
#include <algorithm>
#include <vector>
#include <random>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::mt19937 rng(std::random_device{}());

    // Shuffle in-place
    std::shuffle(v.begin(), v.end(), rng);
    for (int n : v) std::cout << n << ' ';
    std::cout << '\n';

    // Draw 4 elements from v (after shuffle it is still a valid range)
    std::vector<int> sample;
    std::sample(v.begin(), v.end(),
                std::back_inserter(sample),
                4, rng);

    std::cout << "Sample: ";
    for (int n : sample) std::cout << n << ' ';
    std::cout << '\n';
}

std::sample requires the output iterator to be a std::output_iterator β€” std::back_inserter satisfies this. The algorithm guarantees that every k-element subset has equal probability. Unlike a shuffle followed by a take, std::sample works on forward iterators and is O(n), making it efficient over large ranges.

std::merge and std::inplace_merge

std::merge combines two separately sorted ranges into a single sorted output range in O(n + m) time β€” no extra sorting required. std::inplace_merge does the same job when both sorted halves share a single buffer.

cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> a = {1, 3, 5, 7, 9};
    std::vector<int> b = {2, 4, 6, 8, 10};
    std::vector<int> merged(a.size() + b.size());

    std::merge(a.begin(), a.end(),
               b.begin(), b.end(),
               merged.begin());

    for (int n : merged) std::cout << n << ' ';
    // 1 2 3 4 5 6 7 8 9 10
    std::cout << '\n';

    // inplace_merge: merge two adjacent sorted halves of one vector
    std::vector<int> combined = {1, 3, 5, 2, 4, 6};
    auto mid = combined.begin() + 3;           // points at 2 β€” start of second half
    std::inplace_merge(combined.begin(), mid, combined.end());
    for (int n : combined) std::cout << n << ' ';
    // 1 2 3 4 5 6
    std::cout << '\n';
}

std::inplace_merge may allocate a temporary buffer internally (O(n) extra space). If the allocation fails it falls back to an O(n log n) in-place rotation algorithm. Both merge variants are stable β€” equal elements from the first range appear before those from the second.

Pitfalls: Container-Specific vs Generic Algorithms

Some containers provide member function algorithms that must be used instead of β€” not alongside β€” the generic <algorithm> versions. The classic trap is calling std::sort on a std::list.

cpp
#include <list>
#include <algorithm>

std::list<int> lst = {3, 1, 4, 1, 5};

// WRONG β€” does not compile: std::sort requires random-access iterators
// std::list provides only bidirectional iterators
std::sort(lst.begin(), lst.end());  // compile error

// CORRECT β€” list has a member sort that works with bidirectional iterators
lst.sort();                          // O(n log n), stable

The same applies to std::list::unique (removes consecutive duplicates β€” call .sort() first) and std::list::remove_if (the generic erase-remove idiom is needed for std::vector, but list::remove_if does it in one step). Similarly, std::map and std::set have .count() and .find() members that are O(log n) β€” using std::find on them is O(n) and misses the tree index entirely.

What's Next

With these building blocks in hand, you are ready for more specialized territory:

  • Fold operations β€” std::reduce, std::inclusive_scan, and parallel-friendly folding from C++17.
  • Modifying algorithms β€” std::fill, std::generate, std::replace_if, and other in-place mutation tools.
  • Min/max algorithms β€” std::clamp, std::minmax_element, and reading results cleanly with structured bindings.
  • Merge algorithms β€” combining two sorted ranges without a temporary container.
  • Heap operations β€” building priority queues on the fly and performing partial sorts with std::make_heap and std::partial_sort.