Non-Modifying Sequence Algorithms
C++ non-modifying algorithms — for_each, count, count_if, any_of, all_of, none_of, find, find_if, mismatch, equal, and their C++20 ranges equivalents.
Non-Modifying Sequence Algorithmssince C++98Algorithms in <algorithm> that inspect or traverse a range without changing any element. They take iterator pairs (or C++20 range objects) and a predicate or value, and return an iterator, count, or boolean result.
Overview
Non-modifying algorithms answer questions about a range: does it contain X? how many elements satisfy a condition? do two ranges agree? They form the core of expressive, loop-free C++ and are the foundation for most STL usage patterns.
All non-modifying algorithms have C++20 std::ranges:: equivalents that accept the container directly and support projections.
Applying a Function: for_each
#include <algorithm>
#include <vector>
std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), [](int x) {
std::cout << x << ' ';
});
// 1 2 3 4 5
// C++20 ranges — pass the container directly
std::ranges::for_each(v, [](int x) { std::cout << x << ' '; });
// for_each returns the function object (useful for stateful lambdas)
int total = 0;
auto fn = std::for_each(v.begin(), v.end(), [&total](int x) { total += x; });
// total == 15Use std::for_each for side effects. For transforming elements to a new range, prefer std::transform.
Counting: count and count_if
std::vector<int> scores = {85, 92, 78, 95, 60, 88, 72};
// Count exact value
auto zeros = std::count(scores.begin(), scores.end(), 0); // 0
// Count matching predicate
auto passing = std::count_if(scores.begin(), scores.end(),
[](int s) { return s >= 70; }); // 6
// C++20 ranges
auto high = std::ranges::count_if(scores, [](int s) { return s >= 90; }); // 2Return type is std::ptrdiff_t (a signed integer). Cast to std::size_t if you need to compare with container sizes.
Existence Tests: any_of, all_of, none_of
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2};
bool any_even = std::any_of(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
bool all_pos = std::all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool none_neg = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });
// Short-circuit: any_of stops at the first match, all_of at the first failureAll three return true on an empty range when the result is vacuously true: all_of and none_of on {} return true; any_of on {} returns false.
Finding: find, find_if, find_if_not
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
// Find by value
auto it = std::find(names.begin(), names.end(), "Bob");
if (it != names.end()) std::cout << "Found: " << *it << '\n';
// Find by predicate
auto it2 = std::find_if(names.begin(), names.end(),
[](const std::string& s) { return s.size() > 4; });
// it2 points to "Alice"
// Find the first element NOT satisfying pred (C++11)
auto it3 = std::find_if_not(names.begin(), names.end(),
[](const std::string& s) { return s[0] >= 'A' && s[0] <= 'Z'; });
// it3 == end() — all start with uppercase
// C++23: find_last variants
// auto it4 = std::ranges::find_last_if(names, pred).begin();Always check it != end() before dereferencing — not found returns the end iterator.
Range Comparison: equal, mismatch, lexicographical_compare
std::vector<int> a = {1, 2, 3, 4};
std::vector<int> b = {1, 2, 3, 4};
std::vector<int> c = {1, 2, 9, 4};
// Are ranges identical?
bool same = std::equal(a.begin(), a.end(), b.begin()); // true
bool same2 = std::equal(a.begin(), a.end(), c.begin()); // false
// Equal with custom comparator
bool approx = std::equal(a.begin(), a.end(), b.begin(),
[](int x, int y) { return std::abs(x - y) <= 1; });
// Find the first differing position
auto [pa, pb] = std::mismatch(a.begin(), a.end(), c.begin());
// *pa == 3, *pb == 9 — first mismatch
// Lexicographic comparison (< based on first diff)
bool less = std::lexicographical_compare(
a.begin(), a.end(),
c.begin(), c.end()); // true: a < c because 3 < 9 at index 2std::equal with a 3-iterator form requires the second range to be at least as long as the first. The 4-iterator form (C++14) is safer — it stops when either range is exhausted.
Adjacency Tests
// Find the first consecutive duplicate
auto it = std::adjacent_find(v.begin(), v.end());
// Find first pair satisfying custom predicate
auto it2 = std::adjacent_find(v.begin(), v.end(),
[](int a, int b) { return b - a > 5; }); // first "big jump"C++20 Ranges Equivalents
Every non-modifying algorithm has a std::ranges:: counterpart that:
- Accepts a range object directly (no
begin/endpair) - Supports projections (apply a function before comparison)
- Returns
std::ranges::in_*result types rather than bare iterators
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Carol", 35}};
// Find youngest person (projection example)
auto it = std::ranges::min_element(people, {}, &Person::age);
// it->name == "Bob"
// Count people over 28, looking only at age
auto n = std::ranges::count_if(people,
[](int age) { return age > 28; },
&Person::age); // projection: extract age before predicateCommon Pitfalls
Treating any_of on empty range as false
std::all_of({}, pred) returns true (vacuous truth). std::any_of({}, pred) returns false. Surprising but correct by mathematical convention.
Assuming std::find is O(log n)
std::find and std::find_if are always O(n). For O(log n) lookup on a sorted range, use std::binary_search or std::lower_bound.
Ignoring the return value of for_each
std::for_each returns the function object, which is useful when the lambda is stateful:
struct Accumulator {
int sum = 0;
void operator()(int x) { sum += x; }
};
Accumulator acc = std::for_each(v.begin(), v.end(), Accumulator{});
// acc.sum == totalProjections: Search Without a Lambda
Every std::ranges:: non-modifying algorithm accepts an optional projection as its last parameter. A projection is any callable that transforms each element before it is compared or tested. The most common projection is a pointer-to-member, which lets you search or test by a specific field without writing a lambda.
#include <algorithm>
#include <vector>
#include <string>
struct User {
int id;
std::string name;
};
std::vector<User> users = { {1, "Alice"}, {2, "Bob"}, {3, "Carol"} };
// Find by id — no lambda needed
auto it = std::ranges::find(users, 2, &User::id);
// it->name == "Bob"
// Count users whose name has more than 4 characters
auto n = std::ranges::count_if(
users,
[](std::size_t len) { return len > 4; },
[](const User& u) { return u.name.size(); }); // projection: extract length
// n == 2 (Alice=5, Carol=5)
// Check whether any user has id > 2
bool any_senior = std::ranges::any_of(users,
[](int id) { return id > 2; },
&User::id); // project to id, then test
// all_of / none_of with projection work the same way
bool all_named = std::ranges::all_of(users,
[](const std::string& s) { return !s.empty(); },
&User::name);Projections are invoked via std::invoke, so they accept lambdas, pointer-to-data-member, and pointer-to-member-function equally. When you pass {} as the comparator/predicate slot, it defaults to std::ranges::equal_to{} or std::ranges::less{} depending on the algorithm. Passing {} explicitly lets you skip straight to the projection argument.
C++23: contains and contains_subrange
Before C++23, checking membership required std::find(v.begin(), v.end(), x) != v.end() — a pattern that reads awkwardly. C++23 adds a direct boolean test.
#include <algorithm>
#include <array>
#include <vector>
std::vector<int> primes = {2, 3, 5, 7, 11, 13};
// Simple membership test (C++23)
bool has7 = std::ranges::contains(primes, 7); // true
bool has10 = std::ranges::contains(primes, 10); // false
// With projection: does the vector contain a user named "Bob"?
struct User { int id; std::string name; };
std::vector<User> users = { {1, "Alice"}, {2, "Bob"} };
bool has_bob = std::ranges::contains(users, std::string{"Bob"}, &User::name);
// true
// Subrange check: does haystack contain the needle as a contiguous subsequence?
std::array haystack = {1, 2, 3, 4, 5};
std::array needle = {2, 3, 4};
std::array bodkin = {1, 3, 5}; // non-contiguous, not a subrange
bool yes = std::ranges::contains_subrange(haystack, needle); // true
bool no = std::ranges::contains_subrange(haystack, bodkin); // falsestd::ranges::contains is equivalent to std::ranges::find(r, value, proj) != std::ranges::end(r) but expresses intent directly. std::ranges::contains_subrange uses std::ranges::search internally and checks for a contiguous match — it is O(N*M) in the worst case.
Both are defined in <algorithm> and require C++23 (__cpp_lib_ranges_contains >= 202207L).
C++23: find_last, find_last_if, find_last_if_not
The classic std::find family always finds the first matching element. C++23 adds a symmetric family for finding the last match.
#include <algorithm>
#include <vector>
#include <string>
std::vector<int> v = {1, 2, 3, 1, 2, 3, 1, 2};
// find_last returns a subrange [it, end), not a bare iterator
auto sub = std::ranges::find_last(v, 3);
// sub.begin() points to the second '3' (index 5)
// *sub.begin() == 3
// find_last_if: last element satisfying a predicate
auto even = std::ranges::find_last_if(v, [](int x) { return x % 2 == 0; });
// *even.begin() == 2 (the last even element, index 7)
// find_last_if_not: last element NOT satisfying the predicate
auto odd_last = std::ranges::find_last_if_not(v, [](int x) { return x % 2 == 0; });
// *odd_last.begin() == 1 (last odd element, index 6)
// Empty subrange when not found
auto missing = std::ranges::find_last(v, 99);
// missing.begin() == v.end() — not found
// With projection: find last User with id == 2
struct User { int id; std::string name; };
std::vector<User> users = { {1,"A"}, {2,"B"}, {1,"C"}, {2,"D"} };
auto last2 = std::ranges::find_last(users, 2, &User::id);
// last2.begin()->name == "D"The return type is std::ranges::subrange<I> — a [begin, end) pair. Access the found element via .begin(), and check for "not found" with .begin() == range.end(). These algorithms require a forward range (not just input iterators). On bidirectional or random-access iterators, implementations can walk backwards for better performance.
C++23: starts_with and ends_with
These range algorithms generalise std::string::starts_with / std::string::ends_with (C++20) to work with any range, not just strings.
#include <algorithm>
#include <array>
#include <string_view>
#include <vector>
// Check string prefix/suffix using range algorithms
std::string_view sv = "constexpr";
bool starts = std::ranges::starts_with(sv, std::string_view{"const"}); // true
bool ends = std::ranges::ends_with(sv, std::string_view{"expr"}); // true
// Works on any range, not just strings
std::array seq = {1, 2, 3, 4, 5};
std::array prefix = {1, 2, 3};
std::array suffix = {4, 5};
std::array other = {3, 4};
bool sp = std::ranges::starts_with(seq, prefix); // true
bool se = std::ranges::ends_with(seq, suffix); // true
bool no = std::ranges::starts_with(seq, other); // false — other is not a prefix
// With custom predicate: case-insensitive prefix check
auto ci_eq = [](char a, char b) {
return std::tolower(a) == std::tolower(b);
};
bool ci = std::ranges::starts_with(
std::string_view{"Hello World"},
std::string_view{"hello"},
ci_eq); // trueends_with requires a bidirectional range for the haystack so it can work backwards from the end. starts_with only requires input iterators. Both return bool and are O(min(N1, N2)) in the common case.
Both live in <algorithm> and require C++23 (__cpp_lib_ranges_starts_ends_with >= 202106L).
C++23: Fold Algorithms
C++23 adds a "rangified" family of fold operations under <algorithm> to replace std::accumulate (from <numeric>). The core function is std::ranges::fold_left.
#include <algorithm>
#include <vector>
#include <string>
#include <functional>
std::vector<int> nums = {1, 2, 3, 4, 5};
// fold_left: left fold (left to right), replaces std::accumulate
int sum = std::ranges::fold_left(nums, 0, std::plus<>{});
// computes ((((0+1)+2)+3)+4)+5 == 15
// fold_left with a non-commutative operation (string concatenation)
std::vector<std::string> words = {"Hello", " ", "World"};
auto sentence = std::ranges::fold_left(words, std::string{}, std::plus<>{});
// "Hello World"
// fold_right: right-to-left fold
// For fold_right the operator receives (element, accumulator), not (accumulator, element)
std::vector<int> v = {1, 2, 3};
int rfold = std::ranges::fold_right(v, 0, std::plus<>{});
// computes 1+(2+(3+0)) == 6 — same as fold_left for +
// For non-associative operations the result differs from fold_left
// fold_left_first: use first element as the initial value (no identity needed)
// Returns std::optional<T> — empty if the range is empty
auto maybe_sum = std::ranges::fold_left_first(nums, std::plus<>{});
// maybe_sum.value() == 15
// fold_right_last: same idea but right fold, uses last element as initial
auto maybe_r = std::ranges::fold_right_last(nums, std::plus<>{});
// maybe_r.value() == 15The six fold algorithms in C++23:
| Algorithm | Direction | Initial value |
|---|---|---|
fold_left | left to right | explicit |
fold_right | right to left | explicit |
fold_left_first | left to right | first element (optional result) |
fold_right_last | right to left | last element (optional result) |
fold_left_with_iter | left to right | explicit; returns {end_iter, value} |
fold_left_first_with_iter | left to right | first element; returns {end_iter, optional} |
Unlike most ranges algorithms, fold algorithms do not support projections directly (by design — it would force extra copies in the _first/_last variants). Compose with std::views::transform when a projection is needed:
// Sum just the .id fields:
struct User { int id; std::string name; };
std::vector<User> users = { {1,"A"}, {2,"B"}, {3,"C"} };
int id_sum = std::ranges::fold_left(
users | std::views::transform(&User::id),
0, std::plus<>{});
// 6fold_left is defined in <algorithm> (C++23). Use __cpp_lib_ranges_fold >= 202207L to detect support.
Unified std::ranges Non-Modifying Tour
The std::ranges versions of the classic non-modifying algorithms share a uniform calling convention. Each accepts either a range or an iterator-sentinel pair, an optional comparator or predicate, and an optional projection — in that consistent order. Learning the pattern once applies to the entire family.
// C++20
#include <algorithm>
#include <vector>
#include <string>
struct Employee { std::string name; int dept; int salary; };
std::vector<Employee> staff = {
{"Ana", 1, 80000}, {"Ben", 2, 95000},
{"Cara", 1, 72000}, {"Dan", 2, 85000},
};
// ranges::find — locate first element matching a value via projection
auto it = std::ranges::find(staff, 2, &Employee::dept);
// it->name == "Ben" (first employee in dept 2)
// ranges::count_if — count with projection-aware predicate
auto high_earners = std::ranges::count_if(
staff,
[](int s) { return s >= 85000; },
&Employee::salary); // project to salary, then test
// high_earners == 2 (Ben and Dan)
// ranges::all_of — verify every element via projection
bool all_paid = std::ranges::all_of(
staff,
[](int s) { return s > 0; },
&Employee::salary); // true — all have positive salaries
// ranges::any_of — short-circuit existence check with projection
bool has_dept1 = std::ranges::any_of(staff, [](int d) { return d == 1; }, &Employee::dept);
// true — Ana and Cara are in dept 1Projections are especially powerful when the search key lives inside a nested struct field — they eliminate the need to write a wrapping lambda just to extract the field before comparison.
std::ranges::contains as a find-Based Idiom Replacement (C++23)
Before C++23, every membership check required a find call followed by an end-iterator comparison — readable once you know the idiom, but noisy for newcomers. std::ranges::contains names the intent directly and composes with projections the same way every other ranges algorithm does.
// C++20 — pre-contains idiom
#include <algorithm>
#include <vector>
std::vector<int> ids = {10, 20, 30, 40};
// Old way: two-step, easy to forget the != end() check
bool found_old = std::ranges::find(ids, 20) != ids.end(); // true
// C++23 — contains: one call, obvious semantics
bool found_new = std::ranges::contains(ids, 20); // true
bool missing = std::ranges::contains(ids, 99); // false
// Contains with projection — search a struct range by field value
struct Tag { int id; std::string label; };
std::vector<Tag> tags = { {1,"alpha"}, {2,"beta"}, {3,"gamma"} };
bool has_beta = std::ranges::contains(tags, std::string{"beta"}, &Tag::label);
// true — equivalent to ranges::find(tags, "beta", &Tag::label) != tags.end()
// but without the boilerplate comparisonstd::ranges::contains does not require the range to be sorted; it performs a linear scan identical to std::ranges::find. For sorted ranges, std::ranges::binary_search remains the O(log n) alternative. Feature-test macro: __cpp_lib_ranges_contains >= 202207L.
See Also
- Searching Algorithms —
std::binary_search,std::lower_bound,std::upper_boundfor sorted ranges - Modifying Algorithms —
std::transform,std::remove_if,std::copy_if - Algorithms Overview — choosing the right algorithm family
- Ranges and Views — C++20 ranges equivalents with projections and composable views