You meet std::sort(items.begin(), items.end(), [capture](...) { ... }) and the comparison itself makes sense. But what do the square brackets store, and how long does that stored state remain valid?
For a first pass through syntax, value-versus-reference capture, and broad STL use, start with Lambda Expressions in C++: Syntax, Captures and Worked Examples. Mixed closure state, strict-order sort comparators, and ownership checks need a deeper trace when callbacks may outlive their creation scope.
Read a lambda from left to right
The full anatomy is:
[capture](parameters) mutable -> return_type { body }Only the capture list, parameter list, and body are needed in the common form. mutable and the trailing return type are optional tools.
A lambda expression creates a callable object of an unnamed closure type. Captured values or references become state inside that object. This is the useful connection to object-oriented thinking: the closure has behaviour, its call operator, and may also have stored data.
int limit = 10;
auto above = [limit](int x) { return x > limit; };The closure holds its own limit = 10. Therefore, above(12) is true, while above(7) is false.
Capture by value, by reference, and in a mixed list
Consider an explicit mixed capture:
int threshold = 10;
int hits = 0;
auto accept = [threshold, &hits](int x) {
if (x >= threshold) {
++hits;
return true;
}
return false;
};
threshold = 20;At creation, the closure copies threshold, so its stored value remains 10. It stores a reference to the outer hits instead.
Now trace the calls:
accept(12)checks12 >= 10, not12 >= 20. The condition is true, so outerhitschanges from0to1, and the call returnstrue.accept(9)checks9 >= 10. The condition is false, sohitsstays1, and the call returnsfalse.
[=] uses value capture by default, while [&] uses reference capture by default. They are concise, but explicit captures such as [threshold, &hits] are easier to review when a body grows or a callback is stored.
The decision rule is simple. Copy small snapshot state when later outside changes must not affect the callback. Capture by reference only when shared mutation is intended and the referenced object is guaranteed to outlive every call.

Why mutable changes a copy, not the outer variable
Init-capture can name and initialise closure state directly:
int seed = 3;
auto next = [n = seed]() mutable { return n++; };
seed = 100;The first call returns 3, then increments the private n to 4. The next two calls return 4 and 5, leaving the closure's n equal to 6. Outer seed remains 100 throughout.
Without mutable, n++ is invalid because the lambda's default call operator cannot modify value-captured state. mutable permits modification of the closure's copy. It does not turn that copy into a reference.
Init-capture can also move a move-only object into closure state, which is useful in more advanced ownership-sensitive callbacks.
A practical std::sort comparator with exact output
Suppose students must be ordered by marks descending, then by name ascending when marks tie:
struct Student {
std::string name;
int marks;
};
std::vector<Student> students{
{"Asha", 72}, {"Ravi", 91}, {"Neha", 72}, {"Kabir", 84}
};
bool descending = true;
std::sort(students.begin(), students.end(),
[descending](const Student& a, const Student& b) {
if (a.marks != b.marks) {
return descending ? a.marks > b.marks : a.marks < b.marks;
}
return a.name < b.name;
});The exact output is:
Ravi 91
Kabir 84
Asha 72
Neha 72For Asha 72 and Neha 72, the marks tie, so "Asha" < "Neha" puts Asha first. For equivalent inputs, cmp(Asha72, Asha72) must return false. A comparator that directly returns a.marks >= b.marks would violate the strict ordering required by std::sort. You can compare sorting approaches and complexity separately, but every custom comparator still needs this rule.

Generic lambdas and callback-shaped code
A generic lambda uses auto parameters so each call determines the parameter type:
auto twice = [](auto x) { return x * 2; };Here, twice(6) is 12, and twice(2.5) is 5.0. The body remains one reusable pattern.
Reference capture is appropriate when a callback runs immediately and deliberately updates outer state:
std::vector<int> values{2, 4, 6};
int total = 5;
std::for_each(values.begin(), values.end(),
[&total](int x) { total += x; });The running totals are 7, 11, and 17, so the final total is 17. The lambda completes inside the for_each call while total is alive. A callback stored for later needs a separate lifetime check.
Closure lifetime and the dangling-reference trap
This factory is unsafe:
auto make_times_three() {
int factor = 3;
return [&factor](int x) { return factor * x; };
}When the function returns, local factor no longer exists. The returned closure object may still exist, but its reference dangles. Calling it with 7 has undefined behaviour, so there is no valid output to predict.
Own the snapshot instead:
auto make_times_three() {
int factor = 3;
return [factor](int x) { return factor * x; };
}The closure now owns 3, so input 7 returns 21. Similarly, capturing this does not keep the object alive. For a deferred callback, arrange suitable ownership or disconnect the callback before object destruction.
Common traps and how assessments test them
Mistaken assumption | What fails | Correct rule |
|---|---|---|
| Global state is not copied into closure state | Treat globals separately and inspect what the body reads |
| A stored callback can outlive a referenced local | Use a reference only when its lifetime covers every call |
| Only the captured copy changes | Use reference capture for intentional shared mutation |
A sort comparator may return true for equal items | The relation is not a strict ordering | Return |
Common assessment forms ask you to predict the threshold and hits trace, identify why n++ needs mutable, or choose between [&factor] and [factor] in a returned callback.
An interview-style bug may say that a deferred callback intermittently reads a destroyed local. Identify the reference capture, compare the callback and referent lifetimes, then replace the reference with owned state when snapshot semantics are correct.
The short version and the next practice step
A lambda creates a closure object.
Value capture owns a snapshot.
Reference capture shares state and brings lifetime risk.
mutablepermits changes to owned copies.A comparator must define a strict ordering.
For full syntax-to-practice coverage, use the C++ Programming course. For placement-focused work across callbacks, multiple languages, and coding problems, continue with Coding for Placements.
Now rerun the student sort with descending = false. The exact output should be Asha 72, Neha 72, Kabir 84, Ravi 91. Then change only the tie rule to name descending and predict Neha 72 before Asha 72. The broader Coding & Skills catalogue gives you the next topic to practise after the trace is correct.




