C++ / ITERATORS, ALGORITHMS, AND RANGES
Lambdas with algorithms instead of raw loops
Turn hand-written element loops into count_if, find_if and any_of calls with lambda predicates, and know exactly what your captures hold when they run.
What you will learn
- Replace loop-plus-if bodies with count_if, find_if, any_of, all_of, none_of
- Predict capture behaviour: [x] copies at creation, [&x] reads the value at call time
- Use mutable for a by-value capture, and expect copies of a closure to hold separate state
- Keep predicates pure and bool-returning, because algorithms may copy them
Understanding Lambdas with algorithms instead of raw loops
A lambda is not a special language feature bolted onto algorithms; it is shorthand for an unnamed class with an operator(), where each capture becomes a data member. When you hand that object to count_if or find_if, you have split one job in two: the algorithm owns the walking and the stopping, and your closure owns the single decision about one element. That is why the call site reads as a sentence, count_if(first, last, isHot), while the equivalent loop makes a reader reconstruct the intent from an index, a comparison and a counter that happen to sit next to each other.
The capture list is where beginners lose the plot, because it is evaluated once, when the closure object is constructed, not when the algorithm calls it. [limit] copies the current value into the member; [&limit] stores a reference, so every call reads whatever limit holds at that moment and the closure must not outlive it. operator() is const by default, so a by-value capture is read-only unless you write mutable, and [n = 0] creates a member the enclosing scope never had.
Because every lambda has its own unique type, the algorithm template is instantiated against that exact type and the call is a direct, ordinarily inlined call, which is the concrete reason a lambda comparator beats a function pointer or a std::function that forces an indirect jump. The other side of that coin is that the algorithm receives your predicate by value and is allowed to copy it as often as it likes, and the _if family also forbids modifying the elements through the predicate. A predicate that answers a question is safe under those rules; one that counts, logs into itself or mutates elements gives results the standard does not pin down.
<algorithm>
<cstddef>
<iostream>
<string>
<vector>
struct Reading {
std::string sensor;
double celsius;
};
int main() {
const std::vector<Reading> readings{
{"intake", 18.5}, {"core", 71.2}, {"exhaust", 64.0},
{"core", 88.9}, {"intake", 21.0}};
const double limit = 70.0;
// Hand-written loop: traversal, the test and the tally are tangled.
int hotByLoop = 0;
for (std::size_t i = 0; i < readings.size(); ++i) {
if (readings[i].celsius > limit) ++hotByLoop;
}
// The test on its own, named once and reused by three algorithms.
auto isHot = [limit](const Reading& r) { return r.celsius > limit; };
const auto hot = std::count_if(readings.begin(), readings.end(), isHot);
const auto firstHot = std::find_if(readings.begin(), readings.end(), isHot);
const bool allSafe = std::none_of(readings.begin(), readings.end(), isHot);
std::cout << "loop tally: " << hotByLoop << "\n";
std::cout << "count_if: " << hot << "\n";
std::cout << "first hot: " << firstHot->sensor
<< " at " << firstHot->celsius << "\n";
std::cout << std::boolalpha;
std::cout << "all safe: " << allSafe << "\n";
std::cout << "closure size == one double: "
<< (sizeof(isHot) == sizeof(double)) << "\n";
}
A lambda is a small object whose captures are its members, and passing it to a named algorithm separates the decision about one element from the traversal of all of them.
Worked examples
When a capture is read
The same test written with a by-value and a by-reference capture gives two different counts after the captured variable changes.
<algorithm>
<iostream>
<vector>
int main() {
std::vector<int> v{3, 9, 4, 12, 7};
int limit = 5;
auto byValue = [limit](int n) { return n > limit; };
auto byRef = [&limit](int n) { return n > limit; };
limit = 8; // changed after both closures were built
std::cout << "by value: "
<< std::count_if(v.begin(), v.end(), byValue) << "\n";
std::cout << "by ref: "
<< std::count_if(v.begin(), v.end(), byRef) << "\n";
}
Example explained
Line 1[limit] copied 5 into a member of byValue on the line that created it, so it still counts 9, 12 and 7.
Line 2[&limit] stored a reference, so the call inside count_if reads the current 8 and counts only 9 and 12.
Line 3Neither closure is re-initialised by count_if; the algorithm just calls operator() once per element.
Line 4The by-reference form is only safe while limit is alive, which is why it must not be stored or returned.
Closures carry state, and copies carry their own
A mutable lambda holding a counter shows why a stateful predicate cannot be trusted inside an algorithm.
<iostream>
int main() {
auto tick = [n = 0]() mutable { return ++n; };
tick();
tick(); // this closure's n is now 2
auto snapshot = tick; // copies the closure, n included
const int fromSnapshot = snapshot();
tick();
const int fromOriginal = tick();
std::cout << "original: " << fromOriginal << "\n";
std::cout << "snapshot: " << fromSnapshot << "\n";
}
Example explained
Line 1[n = 0] is an init-capture: it creates a member named n that no outer variable corresponds to.
Line 2mutable drops the const on the generated operator(), which is the only reason ++n compiles.
Line 3Copying tick copies n, so snapshot continues from 2 independently and reaches 3 while the original reaches 4.
Line 4Algorithms take predicates by value and may copy them, so counters like this end up in a copy you never see.
Building a predicate out of a predicate
A function that captures a lambda by value and returns a new closure, reused by two algorithms.
<algorithm>
<cctype>
<iostream>
<string>
<vector>
template <class Pred>
auto negate_pred(Pred p) {
return [p](const auto& value) { return !p(value); };
}
int main() {
std::vector<std::string> names{"ada", "Grace", "linus", "Bjarne", "ken"};
auto isLower = [](const std::string& s) {
return !s.empty() && std::islower(static_cast<unsigned char>(s.front()));
};
auto isUpper = negate_pred(isLower);
std::cout << "lower: "
<< std::count_if(names.begin(), names.end(), isLower) << "\n";
std::cout << "upper: "
<< std::count_if(names.begin(), names.end(), isUpper) << "\n";
auto it = std::find_if(names.begin(), names.end(), isUpper);
std::cout << "first capitalised: " << *it << "\n";
}
Example explained
Line 1negate_pred needs a template parameter because a lambda's type has no spelling you can write down.
Line 2[p] captures the incoming closure by value, which is what makes returning the new closure safe.
Line 3The returned lambda takes const auto&, so the same helper works for any element type its predicate accepts.
Line 4The standard library ships std::not_fn in <functional> for exactly this negation, once you know how it works.
Important notes
The _if algorithms take the predicate by value and may copy or call it more than you expect, so a predicate must answer a question and must not modify the element it inspects; std::for_each is the one that hands the function object back as its return value.
Each lambda has a unique unnamed type, so auto or a template parameter is the only way to hold one exactly; std::function will store any of them but adds an indirect call the compiler usually cannot inline.
Common mistakes
Writing [&] out of habit and then storing or returning the lambda: the referenced locals are gone by the time count_if calls it, so the predicate reads dead stack memory and the count is garbage or the program crashes.
Trying to tally inside a predicate, as in [count](const Reading& r) mutable { ++count; return r.celsius > 70; }: without mutable it will not compile, and with mutable the algorithm may increment a copy, leaving your original count at zero.
Forgetting the return, as in [limit](int n) { n > limit; }: the return type deduces to void, and the error appears deep inside <algorithm> where the result is used as a condition rather than on your own line.
Try it yourself
Change, predict, then run
Given std::vector<std::string> words{"pin", "pipeline", "grep", "pointer", "cat"} and int maxLen = 4, print the number of words longer than maxLen, the first word starting with 'p', and whether none of them are empty, using three algorithms with lambdas and no hand-written loop.
Open the C++ workspaceCheck your understanding
A helper returns a predicate: auto makeHot(double limit) { return [&limit](const Reading& r) { return r.celsius > limit; }; }. It compiles without warnings. What happens when the returned predicate is used with count_if?
- Undefined behaviour: the closure holds a reference to the parameter limit, which died when makeHot returned
- It works, because count_if copies the predicate before calling it, and the copy owns the value
- It works, because limit was passed to makeHot by value, so the closure keeps that copy alive
- It does not compile, because a lambda's type cannot be returned from a function
Show answer
A by-reference capture stores only a reference, and limit is a function parameter with automatic storage duration, so every call through the returned closure reads an object whose lifetime has ended. Option 2 is tempting because algorithms really do copy predicates, but copying the closure just copies the dangling reference; the parameter being passed by value only means makeHot had its own copy to bind to, which dies with the call. Changing the capture to [limit] stores the double inside the closure and fixes it.