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.
#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
#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.
#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.
#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.
#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_ofon an empty range returnstrue.any_ofon an empty range returnsfalse.none_ofon an empty range returnstrue.
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.
#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.
#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
#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.
// 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
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.
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:
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 behaviorStore indices rather than iterators when you need to reference elements across a potential reallocation.
Quick Reference
| Algorithm | Header | What 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.
#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 betterThe 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.
#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.
#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:
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.
#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:
| Algorithm | Complexity | Notes |
|---|---|---|
std::sort | O(n log n) | Introsort in practice; standard says O(n log n) worst case since C++11 |
std::stable_sort | O(n logΒ² n) or O(n log n) | O(n log n) if extra memory available |
std::partial_sort | O(n log k) | k = distance to middle iterator |
std::nth_element | O(n) average | Linear on average; O(nΒ²) worst case allowed |
std::find / find_if | O(n) | Linear scan, no preconditions |
std::binary_search | O(log n) | Requires sorted range |
std::lower_bound | O(log n) | Requires sorted range |
std::merge | O(n + m) | Linear in total input size |
std::set_intersection | O(n + m) | Linear in total input size |
std::unique | O(n) | Collapses adjacent duplicates |
std::count_if | O(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.
#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.
#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.
#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), stableThe 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_heapandstd::partial_sort.