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:
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:
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:
#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 elementsFinding elements
#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
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); // 0Sorting
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, Carolstd::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:
#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:
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
#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()); // 15Common Patterns
Pattern 1 — collect matching elements into a new vector
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
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 = 9Pattern 3 — fill and generate
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 18Pattern 4 — partition
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 >= 5What Can Go Wrong
Forgetting the erase half of erase-remove
// 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:
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); // correctPredicate 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
| Algorithm | What 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_of | Test 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_element | Iterator 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.
#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.
#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.
#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 maximumTo 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.
#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.
#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.
#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 permutationBoth 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 <=.
#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, lowFor multi-key comparisons, use the std::tie idiom to build lexicographic comparisons safely:
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.
#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.