Why does stderase_if with a mutable predicate function have different behavior for older compilers?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
If a mutable predicate behaves differently across compiler or standard library versions, the real issue is usually not that std::erase_if changed its mathematical meaning. The problem is that stateful predicates rely on details such as copying, moving, and invocation order, and those details are not a safe portability boundary for code that mutates predicate state.
What std::erase_if Actually Does
For sequence containers, std::erase_if is conceptually a convenience wrapper around the erase-remove pattern. The predicate is invoked on elements, and matching elements are removed.
A simple example is fine because the predicate is stateless:
This is predictable because the lambda depends only on the current element.
Why Mutable Predicates Are Different
A mutable predicate carries internal state and changes it over time. That means the result can depend on exactly how many times the predicate object is copied, moved, or called.
For example:
At first glance, you might expect every second element to be removed. But that expectation assumes one predicate object with one stable count variable and one predictable call sequence. That assumption is weaker than many people realize.
Copies and Moves Matter
Algorithms are generally allowed to copy function objects. Standard library implementations may store the predicate, forward it, wrap it, or optimize around it in different ways. If the predicate mutates internal state, every copy can produce a new state history.
That is why an older compiler or standard library may appear to "behave differently" even though both are conforming enough for normal predicates. The code is depending on object identity and call history in a place where those details are not the right abstraction.
A simple illustration shows the danger more clearly:
If this object is copied, each copy has its own count. Once that happens, "remove every second element" stops being a well-defined expectation.
Older Compilers Expose the Fragility More Easily
Older compilers and older standard library implementations may differ in:
- how aggressively they inline or wrap algorithms
- how often they copy function objects internally
- how they implement library helpers used by erase-remove style operations
- how complete their C++20 library support is
Those differences are usually harmless for pure predicates. They become visible when the predicate is stateful and mutable.
So the difference is less about a special rule for older compilers and more about undefined expectations in the caller's design.
Write Predicates as Pure Functions
The robust fix is to make the predicate depend only on the element value, not on hidden internal state.
Good:
Risky:
If you truly need position-based removal, compute positions explicitly instead of encoding them in a mutable predicate.
That code makes the intent explicit and portable.
If You Need External State, Control It Explicitly
Sometimes a predicate does need context, but even then the safest pattern is to keep that state external and stable rather than hidden inside a copied function object.
For example:
Capturing read-only values is fine. The danger is mutable internal evolution during the algorithm itself.
Common Pitfalls
- Assuming a mutable predicate will be called through one single object instance is unsafe because algorithms may copy function objects.
- Depending on call order inside the predicate makes the result fragile even if the compiler happens to produce the expected output today.
- Blaming only the compiler version misses the real design issue. The code is relying on mutable predicate state where a pure predicate is the correct abstraction.
- Using
std::erase_iffor position-based removal is the wrong tool. If the rule depends on element index, write index-based logic directly. - Testing only on one standard library implementation can hide portability problems until an older compiler or different STL exposes them.
Summary
- '
std::erase_ifworks best with pure predicates that depend only on the current element.' - Mutable predicates can behave differently because function objects may be copied or moved internally.
- Older compilers often expose that fragility more clearly, but the core issue is the stateful predicate design.
- If removal depends on position or evolving state, express that logic outside the predicate.
- For portable code, treat predicates as stateless filters, not mini state machines.

