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

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++98

Algorithms 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

cpp
#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 == 15

Use std::for_each for side effects. For transforming elements to a new range, prefer std::transform.

Counting: count and count_if

cpp
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; });  // 2

Return 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

cpp
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 failure

All 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

cpp
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

cpp
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 2

std::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

cpp
// 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/end pair)
  • Supports projections (apply a function before comparison)
  • Returns std::ranges::in_* result types rather than bare iterators
cpp
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 predicate

Common 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:

cpp
struct Accumulator {
    int sum = 0;
    void operator()(int x) { sum += x; }
};

Accumulator acc = std::for_each(v.begin(), v.end(), Accumulator{});
// acc.sum == total

Projections: 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.

cpp
#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.

cpp
#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);  // false

std::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.

cpp
#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.

cpp
#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);  // true

ends_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.

cpp
#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() == 15

The six fold algorithms in C++23:

AlgorithmDirectionInitial value
fold_leftleft to rightexplicit
fold_rightright to leftexplicit
fold_left_firstleft to rightfirst element (optional result)
fold_right_lastright to leftlast element (optional result)
fold_left_with_iterleft to rightexplicit; returns {end_iter, value}
fold_left_first_with_iterleft to rightfirst 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:

cpp
// 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<>{});
// 6

fold_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.

cpp
// 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 1

Projections 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.

cpp
// 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 comparison

std::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