Skip to content
C++
Library
since C++98
Basic

STL Algorithms: Replace Your Loops with Named Operations

Learn to use the C++ standard algorithms library — sort, find, transform, remove_if, count_if, and more — and understand when a named algorithm beats a hand-written loop every time.

By the end of this page you will use std::sort, std::find_if, std::transform, std::remove_if, std::count_if, and std::accumulate confidently, combine them with lambdas, and understand why named algorithms make your intent clearer than hand-written loops.

What and Why

Every C++ programmer writes loops like:

cpp
bool found = false;
for (const auto& item : v) {
    if (item.price > 100) { found = true; break; }
}

The standard algorithms library in <algorithm> and <numeric> provides named operations that replace exactly these patterns:

cpp
bool found = std::any_of(v.begin(), v.end(),
    [](const auto& item) { return item.price > 100; });

Named algorithms have three advantages over hand-written loops: they communicate intent in the name, they are battle-tested implementations (no off-by-one errors), and the compiler knows how to optimise them.

Step by Step

The iterator-pair interface

All standard algorithms work on half-open ranges [first, last) — an iterator to the first element and one past the last. You get these from begin()/end() on any container:

cpp
#include <algorithm>
#include <vector>

std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end());   // sorts entire container
std::sort(v.begin(), v.begin() + 3);   // sorts only first 3 elements

Finding elements

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

std::vector<std::string> names = {"Alice", "Bob", "Carol", "Dave"};

// Find by value
auto it = std::find(names.begin(), names.end(), "Carol");
if (it != names.end()) {
    std::cout << "Found: " << *it << '\n';
}

// Find by condition (predicate)
auto it2 = std::find_if(names.begin(), names.end(),
    [](const std::string& s) { return s.size() > 3; });
// it2 points to "Alice" (first name with length > 3)

// Check if any/all/none satisfy a condition
bool any_long  = std::any_of(names.begin(), names.end(),
    [](const std::string& s) { return s.size() > 4; });
bool all_short = std::all_of(names.begin(), names.end(),
    [](const std::string& s) { return s.size() < 10; });

Always check it != end() before dereferencing — if nothing matches, find/find_if return the end iterator.

Counting

cpp
std::vector<int> scores = {85, 92, 78, 95, 60, 88, 72};

int passing = std::count_if(scores.begin(), scores.end(),
    [](int s) { return s >= 70; });   // 6

int perfect = std::count(scores.begin(), scores.end(), 100);  // 0

Sorting

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

std::sort(v.begin(), v.end());                          // ascending
std::sort(v.begin(), v.end(), std::greater<int>{});     // descending

// Sort by custom comparator
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Bob", 30}, {"Alice", 25}, {"Carol", 35}};

std::sort(people.begin(), people.end(),
    [](const Person& a, const Person& b) { return a.age < b.age; });
// now sorted by age: Alice, Bob, Carol

std::stable_sort preserves the relative order of equal elements — useful when your key is age and you want alphabetical order within the same age group.

Transforming

std::transform applies a function to each element and writes results to an output range:

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

std::vector<std::string> words = {"hello", "world", "foo"};
std::vector<std::size_t> lengths(words.size());

std::transform(words.begin(), words.end(), lengths.begin(),
    [](const std::string& s) { return s.size(); });
// lengths = {5, 5, 3}

// Transform in place
std::transform(words.begin(), words.end(), words.begin(),
    [](std::string s) {
        std::transform(s.begin(), s.end(), s.begin(), ::toupper);
        return s;
    });
// words = {"HELLO", "WORLD", "FOO"}

Removing elements

std::remove_if does not actually remove elements — it moves non-matching elements to the front and returns an iterator to the new logical end. You must erase the tail separately:

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

// Erase-remove idiom (pre-C++20)
v.erase(
    std::remove_if(v.begin(), v.end(), [](int x) { return x < 0; }),
    v.end()
);
// v = {1, 3, 5}

// C++20: std::erase_if is cleaner
std::erase_if(v, [](int x) { return x < 0; });

Accumulating and reducing

cpp
#include <numeric>
#include <vector>

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

int sum     = std::accumulate(v.begin(), v.end(), 0);      // 15
int product = std::accumulate(v.begin(), v.end(), 1,
    [](int acc, int x) { return acc * x; });               // 120

// C++17: std::reduce — same as accumulate but can run in parallel
int sum2 = std::reduce(v.begin(), v.end());                // 15

Common Patterns

Pattern 1 — collect matching elements into a new vector

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

std::copy_if(src.begin(), src.end(), std::back_inserter(positives),
    [](int x) { return x > 0; });
// positives = {1, 3, 5}

std::back_inserter creates an output iterator that appends to the vector — avoids pre-sizing.

Pattern 2 — check container sorted / find min-max

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

bool is_sorted = std::is_sorted(v.begin(), v.end());     // false

auto [min_it, max_it] = std::minmax_element(v.begin(), v.end());
// *min_it = 1, *max_it = 9

Pattern 3 — fill and generate

cpp
std::vector<int> v(10);
std::iota(v.begin(), v.end(), 0);           // 0 1 2 3 4 5 6 7 8 9

std::fill(v.begin(), v.end(), 42);          // 42 42 42 ...

std::generate(v.begin(), v.end(), [n = 0]() mutable { return n++ * 2; });
// 0 2 4 6 8 10 12 14 16 18

Pattern 4 — partition

cpp
std::vector<int> v = {5, 2, 8, 1, 9, 3, 7};
auto mid = std::partition(v.begin(), v.end(), [](int x){ return x < 5; });
// Elements < 5 come first; mid points to first element >= 5

What Can Go Wrong

Forgetting the erase half of erase-remove

cpp
// WRONG — elements are logically removed but the vector is still the same size
std::remove_if(v.begin(), v.end(), pred);   // returns new end, ignored

// RIGHT
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end());
// or C++20:
std::erase_if(v, pred);

Using algorithms on unordered containers incorrectly

std::binary_search, std::lower_bound, and std::upper_bound require a sorted range. Calling them on an unsorted container gives wrong results with no error:

cpp
std::vector<int> v = {3, 1, 4, 1, 5};
bool found = std::binary_search(v.begin(), v.end(), 4);  // WRONG — undefined on unsorted range

std::sort(v.begin(), v.end());
bool found2 = std::binary_search(v.begin(), v.end(), 4); // correct

Predicate with side effects

Algorithms may call your predicate in any order and any number of times (especially with parallel execution). Do not use predicates that modify external state or have ordering-dependent behaviour.

Quick Reference

AlgorithmWhat it does
std::find(first, last, val)First element equal to val
std::find_if(first, last, pred)First element satisfying pred
std::any_of / all_of / none_ofTest predicate against all elements
std::count(first, last, val)Count elements equal to val
std::count_if(first, last, pred)Count elements satisfying pred
std::sort(first, last)Sort range ascending
std::stable_sort(first, last)Sort preserving equal-element order
std::transform(first, last, out, fn)Apply fn to each element, write to out
std::for_each(first, last, fn)Call fn on each element (side effects)
std::copy_if(first, last, out, pred)Copy matching elements to out
std::remove_if(first, last, pred)Partition non-matching to front
std::accumulate(first, last, init)Fold left with + (or custom op)
std::fill(first, last, val)Set every element to val
std::iota(first, last, start)Fill with start, start+1, ...
std::min_element / max_elementIterator to smallest/largest
std::binary_search(first, last, val)Bool: sorted range contains val
std::erase_if(container, pred)Remove matching elements (C++20)

What's Next

  • Lambda Expressions — the predicates and functions you pass to algorithms.
  • Iterators — how begin/end iterators and iterator categories determine which algorithms you can use.
  • Ranges and Views — C++20 std::ranges::sort(v) drops the begin/end pair; views add lazy composition on top.

Parallel Algorithms

C++17 added execution policies that let you request parallel or vectorized execution of most standard algorithms with a single extra argument. Pass std::execution::par for multi-threaded parallel execution, std::execution::seq to force sequential execution, or std::execution::par_unseq for parallel plus SIMD vectorization. The runtime decides how many threads to use; you gain parallelism without writing thread management code.

cpp
#include <algorithm>
#include <execution>
#include <vector>

std::vector<double> data(10'000'000);
// populate data ...

// Sort 10 million elements using all available CPU cores
std::sort(std::execution::par, data.begin(), data.end());

// Parallel transform — apply fn to each element concurrently
std::transform(std::execution::par_unseq,
               data.begin(), data.end(), data.begin(),
               [](double x) { return x * x; });

Not every algorithm supports every policy — check your standard library documentation. Because threads execute concurrently, predicates passed to parallel algorithms must be free of data races and must not throw exceptions (throwing terminates the program under a parallel policy). Link against a threading library (-ltbb on Linux with Intel TBB, or use MSVC / libc++ which bundle their own) to actually get parallelism at runtime.

Numeric Algorithms

The <numeric> header provides reduction, prefix-sum, and inner-product algorithms that would otherwise require error-prone manual loops. std::accumulate folds a range left-to-right with an initial value; std::reduce (C++17) does the same but allows reordering, enabling parallel execution. std::inner_product computes a dot product, and std::partial_sum computes prefix sums in place.

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

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

// Sum
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15

// Product (custom binary op)
int product = std::accumulate(v.begin(), v.end(), 1,
    [](int acc, int x) { return acc * x; });        // 120

// Parallel-friendly reduce (C++17) — same result, reordering allowed
int sum2 = std::reduce(v.begin(), v.end(), 0);      // 15

// Dot product of two vectors
std::vector<int> w = {2, 2, 2, 2, 2};
int dot = std::inner_product(v.begin(), v.end(), w.begin(), 0);
// 1*2 + 2*2 + 3*2 + 4*2 + 5*2 = 30

// Prefix sums (in-place)
std::vector<int> prefix(v.size());
std::partial_sum(v.begin(), v.end(), prefix.begin());
// prefix = {1, 3, 6, 10, 15}

Note that std::accumulate processes elements strictly left-to-right and uses the type of init as the accumulation type — passing 0 when summing a vector of double silently computes in integer arithmetic and truncates. Pass 0.0 to accumulate as double.

Heap Operations

The STL heap algorithms operate on a range arranged as a max-heap: the first element is always the largest, and insertion or removal takes O(log n) time. std::make_heap rearranges an existing vector into a valid heap in O(n); std::push_heap adds the last element into the heap structure; std::pop_heap moves the maximum to the last position so you can pop_back it. This is exactly how std::priority_queue works internally.

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

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

std::make_heap(v.begin(), v.end());
std::cout << v.front() << "\n";   // 9 — maximum is always at front

// Add a new element
v.push_back(7);
std::push_heap(v.begin(), v.end());   // restore heap property
std::cout << v.front() << "\n";       // still 9

// Remove the maximum
std::pop_heap(v.begin(), v.end());    // moves max to back
int max_val = v.back();               // 9
v.pop_back();
std::cout << v.front() << "\n";       // 7 — new maximum

To use a min-heap, pass std::greater<int>{} as the comparator to all three functions consistently. std::priority_queue wraps these same operations in a convenient interface and enforces consistent comparator use, so prefer it unless you need direct vector access alongside heap operations.

std::transform_reduce for Parallel Map-Reduce

std::transform_reduce (C++17) combines a mapping step and a reduction step in a single pass, and accepts an execution policy for automatic parallelism. It is the standard way to compute things like "sum of squares" or "dot product" over large datasets without writing a hand-rolled loop or chaining separate transform and reduce calls.

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

std::vector<double> v(1'000'000);
// fill v with data ...

// Parallel sum of squares
double sum_sq = std::transform_reduce(
    std::execution::par,   // execution policy — run in parallel
    v.begin(), v.end(),
    0.0,                   // identity element for addition
    std::plus<>{},         // reduction operation
    [](double x) { return x * x; }   // transform (map) operation
);

// Two-range version: dot product
std::vector<double> w(v.size(), 1.0);
double dot = std::transform_reduce(
    std::execution::par,
    v.begin(), v.end(), w.begin(),
    0.0                    // init; plus and multiplies are default
);

Because the reduction operation may be applied in any order (including interleaved across threads), it must be both associative and commutative. Floating-point addition is not strictly associative, so results may differ slightly from a sequential fold — this is acceptable for most statistical computations.

Set Operations

STL set operations work on sorted ranges and produce sorted output in linear time, making them far more efficient than nested-loop intersection or union on sorted data. std::set_union, std::set_intersection, std::set_difference, and std::set_symmetric_difference all follow the same interface: two sorted input ranges and an output iterator. Both input ranges must be sorted by the same comparator.

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

std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = {3, 4, 5, 6, 7};
std::vector<int> result;

// Union: all elements from both (no duplicates)
std::set_union(a.begin(), a.end(), b.begin(), b.end(),
               std::back_inserter(result));
// result = {1, 2, 3, 4, 5, 6, 7}

result.clear();

// Intersection: elements in both a and b
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
                      std::back_inserter(result));
// result = {3, 4, 5}

result.clear();

// Difference: elements in a but not b
std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
                    std::back_inserter(result));
// result = {1, 2}

These algorithms work on any sorted range, not just std::set. Sorting a vector and applying std::set_intersection is often faster than using std::set directly because vectors have better cache locality.

Permutation Algorithms

std::next_permutation rearranges elements in place into the next lexicographically greater permutation and returns true; it returns false and wraps to the smallest permutation when the range is already at its maximum. This gives you a clean way to enumerate all permutations of a range without recursion.

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

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

// Enumerate all 6 permutations of {1, 2, 3}
do {
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
} while (std::next_permutation(v.begin(), v.end()));
// Output (in order):
// 1 2 3 / 1 3 2 / 2 1 3 / 2 3 1 / 3 1 2 / 3 2 1

// Start from any permutation and step backwards
std::vector<int> w = {3, 2, 1};
std::prev_permutation(w.begin(), w.end());
// w = {3, 1, 2} — the previous permutation

Both functions require the range to be sorted for the enumeration to start from the very first (or very last) permutation. If you call std::next_permutation on an unsorted range you will only see the permutations reachable from that starting arrangement before the function wraps around.

Custom Comparators and Strict Weak Ordering

Every STL algorithm that accepts a comparator requires it to implement a strict weak ordering — and violating this requirement is undefined behaviour that can silently corrupt data or crash your program. A comparator cmp(a, b) must satisfy: irreflexivity (cmp(a, a) is false), asymmetry (if cmp(a, b) then not cmp(b, a)), and transitivity. The easiest way to remember is: it must behave like <, not <=.

cpp
#include <algorithm>
#include <vector>

struct Task {
    int priority;
    std::string name;
};

// CORRECT — strict weak ordering: returns true when a is strictly less
auto by_priority = [](const Task& a, const Task& b) {
    return a.priority < b.priority;   // strict <, not <=
};

// WRONG — using <= violates irreflexivity (cmp(a,a) must be false)
auto bad_cmp = [](const Task& a, const Task& b) {
    return a.priority <= b.priority;  // undefined behaviour when passed to sort
};

std::vector<Task> tasks = {{3, "low"}, {1, "high"}, {2, "medium"}};
std::sort(tasks.begin(), tasks.end(), by_priority);
// tasks sorted: high, medium, low

For multi-key comparisons, use the std::tie idiom to build lexicographic comparisons safely:

cpp
auto by_priority_then_name = [](const Task& a, const Task& b) {
    return std::tie(a.priority, a.name) < std::tie(b.priority, b.name);
};

std::tie creates a tuple of references and uses tuple's operator<, which provides a correct lexicographic strict weak ordering by construction.

C++20 Ranges Equivalents

C++20 ranges algorithms in std::ranges:: are drop-in upgrades that accept the container directly instead of iterator pairs, and add a projection argument to transform elements before comparison without changing the stored order. Every algorithm has a ranges equivalent with the same name under std::ranges.

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

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

std::vector<Person> people = {{"Bob", 30}, {"Alice", 25}, {"Carol", 35}};

// C++17 — iterator pair, explicit lambda
std::sort(people.begin(), people.end(),
    [](const Person& a, const Person& b) { return a.age < b.age; });

// C++20 — pass container directly; projection selects the sort key
std::ranges::sort(people, {}, &Person::age);
// {} means "default comparator (less)", &Person::age is the projection

// Find with projection
auto it = std::ranges::find(people, "Alice", &Person::name);
if (it != people.end()) {
    std::cout << it->name << " is " << it->age << "\n";
}

// Ranges algorithms also return richer result types
auto result = std::ranges::remove_if(people, [](const Person& p) {
    return p.age > 30;
});
people.erase(result.begin(), result.end());

Ranges algorithms do not yet support execution policies (as of C++23) — to get parallel execution you still need the classic iterator-pair overloads with std::execution::par.