C++
standard algorithms
predicates
reference passing
programming tips

Pass std algos predicates by reference in C

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

C++ standard library algorithms like std::sort, std::find_if, and std::for_each accept predicates (callable objects) by value by default. This means the algorithm copies your predicate, which is usually fine for lambdas and function pointers but can be problematic for stateful functors. To pass predicates by reference, use std::ref or template wrappers.

The Default: Predicates Are Copied

cpp
1#include <algorithm>
2#include <vector>
3#include <iostream>
4
5struct Counter {
6    int count = 0;
7
8    bool operator()(int x) {
9        ++count;
10        return x > 5;
11    }
12};
13
14int main() {
15    std::vector<int> v = {1, 3, 7, 2, 8, 4, 9};
16    Counter counter;
17
18    // std::count_if COPIES the counter
19    auto result = std::count_if(v.begin(), v.end(), counter);
20
21    std::cout << "Matches: " << result << "\n";       // 3
22    std::cout << "Counter: " << counter.count << "\n"; // 0 — original unchanged!
23}

The original counter object shows count = 0 because the algorithm worked on a copy. The copy is discarded when the algorithm returns.

Fix 1: Use std::ref

std::ref creates a reference wrapper that the algorithm copies, but the wrapper refers to the original object:

cpp
1#include <functional>  // for std::ref
2
3Counter counter;
4auto result = std::count_if(v.begin(), v.end(), std::ref(counter));
5
6std::cout << "Matches: " << result << "\n";       // 3
7std::cout << "Counter: " << counter.count << "\n"; // 7 — all elements counted!

std::ref(counter) creates a std::reference_wrapper<Counter> that forwards operator() calls to the original counter object.

Fix 2: Use a Lambda That Captures by Reference

cpp
1int count = 0;
2
3auto result = std::count_if(v.begin(), v.end(), [&count](int x) {
4    ++count;
5    return x > 5;
6});
7
8std::cout << "Matches: " << result << "\n";  // 3
9std::cout << "Count: " << count << "\n";     // 7

Lambdas capturing by reference ([&]) are lightweight and do not suffer from the copy problem because the lambda itself is small (just a pointer to the captured variables), and even when copied, all copies point to the same captured variables.

Fix 3: Retrieve the Functor from the Algorithm

Some algorithms return the functor after use. std::for_each is the notable example:

cpp
1Counter counter;
2counter = std::for_each(v.begin(), v.end(), counter);
3// counter now has the updated state from the internal copy
4
5std::cout << "Counter: " << counter.count << "\n"; // 7

This works because std::for_each returns its functor parameter by value after processing. Most other algorithms (like std::sort, std::count_if) do not return the predicate.

When Does Copying Matter?

Stateless Predicates — Copying Is Fine

cpp
1// Function pointer — trivially copyable
2bool isPositive(int x) { return x > 0; }
3std::count_if(v.begin(), v.end(), isPositive);
4
5// Stateless lambda — trivially copyable
6std::count_if(v.begin(), v.end(), [](int x) { return x > 0; });

Stateful Predicates — Copying Loses State

cpp
1struct Accumulator {
2    double sum = 0;
3    void operator()(double x) { sum += x; }
4};
5
6Accumulator acc;
7std::for_each(v.begin(), v.end(), acc);      // acc.sum is still 0
8std::for_each(v.begin(), v.end(), std::ref(acc));  // acc.sum is updated

Expensive-to-Copy Predicates

cpp
1struct HeavyPredicate {
2    std::vector<int> lookup_table;  // Large data
3
4    bool operator()(int x) const {
5        return std::binary_search(lookup_table.begin(), lookup_table.end(), x);
6    }
7};
8
9HeavyPredicate pred;
10pred.lookup_table.resize(1000000);
11
12// BAD: copies the entire lookup_table
13std::find_if(v.begin(), v.end(), pred);
14
15// GOOD: no copy — references the original
16std::find_if(v.begin(), v.end(), std::ref(pred));

std::ref with Different Algorithms

cpp
1#include <algorithm>
2#include <functional>
3
4struct Filter {
5    std::set<int> seen;
6
7    bool operator()(int x) {
8        // Remove duplicates by tracking seen values
9        if (seen.count(x)) return true;
10        seen.insert(x);
11        return false;
12    }
13};
14
15std::vector<int> v = {1, 2, 2, 3, 3, 3, 4};
16Filter filter;
17
18// Must use std::ref so all copies share the same 'seen' set
19auto new_end = std::remove_if(v.begin(), v.end(), std::ref(filter));
20v.erase(new_end, v.end());
21// v = {1, 2, 3, 4}

Why Algorithms Copy Predicates

The C++ standard specifies that algorithms take predicates by value because:

  1. Algorithms may copy internally: Some algorithms partition work across multiple passes or call the predicate from different internal functions
  2. Thread safety: Copies avoid data races in parallel algorithms (std::execution::par)
  3. Simplicity: Value semantics are simpler for the common case (stateless predicates)

The standard explicitly allows algorithms to copy predicates an unspecified number of times, which is why stateful predicates require std::ref.

Parallel Algorithms and Predicates

With C++17 parallel algorithms, passing stateful predicates by reference requires thread safety:

cpp
1#include <execution>
2#include <mutex>
3
4struct ThreadSafeCounter {
5    std::atomic<int> count{0};
6
7    bool operator()(int x) {
8        count.fetch_add(1, std::memory_order_relaxed);
9        return x > 5;
10    }
11};
12
13ThreadSafeCounter counter;
14auto result = std::count_if(
15    std::execution::par,
16    v.begin(), v.end(),
17    std::ref(counter)
18);

Use std::atomic or std::mutex for shared state in parallel algorithms.

Common Pitfalls

  • Lost state: The most common mistake is expecting a stateful functor to retain modifications after being passed to an algorithm by value. The algorithm operates on a copy, and the original is unchanged.
  • std::ref lifetime: The object referenced by std::ref must outlive the algorithm call. Passing std::ref to a temporary or a local that goes out of scope causes undefined behavior.
  • Predicate purity assumption: The standard assumes predicates do not modify the elements they are called with. Predicates that mutate elements cause undefined behavior with most algorithms.
  • Parallel safety: Using std::ref with std::execution::par requires the predicate to be thread-safe. Unsynchronized mutations to shared state cause data races.
  • std::cref for const: Use std::cref when the predicate should not be modified (const reference). This prevents accidental mutation of the original object.

Summary

  • C++ standard algorithms copy predicates by value — stateful functors lose their state
  • Use std::ref(predicate) to pass by reference and preserve state mutations
  • Lambdas capturing by reference ([&]) naturally avoid the copy problem
  • std::for_each returns the functor, allowing state retrieval without std::ref
  • For parallel algorithms, ensure thread safety when using std::ref with stateful predicates

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.