93 / 163 · C++20 · 12 min
Predicates express value conditions, not which call number
Algorithms may copy predicates, and the number of calls is not equal to a position in the container. Separate stable threshold configuration from position operations; use C++20 erase_if to remove elements matching a value condition, then safely perform one explicit position-based erasure.
In this lesson
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
Finished reading the publisher's Item 2 (standalone PDF, 5 pages) and Item 3 (9 pages). All publicly available text of Item 1 (6 pages) and Item 4 (14 pages) has also been read, but the files end respectively at “Good sepa-” and “foreordained base class”, so completeness of the endings cannot be guaranteed; counted as visible excerpts. Items 5–40 and Appendices A/B unread; the full book text was not obtained, and original GotW articles do not substitute for the book.
Edition, actual reading range, and original sources →Having member data is not the same as depending on history
Item 3 distinguishes two kinds of function objects that are often both called stateful. A predicate whose threshold is fixed after creation and whose judgment depends only on the current element can be copied arbitrarily without changing the answer. In contrast, an object that increments internally on every call and returns true on the third call depends on its own history; if the algorithm copies it, the counters fork.
Therefore the predicate of remove_if must not be treated as an implicit loop cursor. Even if a particular library version happens to call it in the expected way, that observation cannot be elevated to a portable contract. Sharing a counter only solves data consistency among copies; it does not supply the order guarantee that the algorithm never gave.
Deleting by value and deleting by position use different interfaces
This example first deletes low-score records by a stable threshold; the lambda captures the fixed threshold by value so every copy yields the same judgment. C++20 std::erase_if accepts the entire vector, performs the compaction and actual erasure, and returns the number removed. It differs from remove_if, which only accepts an iterator range and does not automatically shrink the container.
The second requirement is to delete the third element of the current sequence—this is position semantics. The code first checks the size, then obtains a valid position via begin()+2 and calls the member erase directly. Thus the code does not depend on how many times the predicate is called, and the boundary conditions sit next to the use of the position; empty containers and those with fewer than three elements will not go out of bounds.
Reinterpret position after the algorithm result
Filtering by value compact the sequence, so the subsequent third element is the third after filtering, not the third of the original input. These are two different business requirements and must not be silently swapped during optimization. If deletion by original position is required, perform the original-position operation first, or store an explicit original index in the input records.
The example checks final values only after the operations complete; it does not observe the unspecified values in the tail after remove_if, nor continue using iterators from before the erasure. The original book used C++98 function-object adapters; modern code can replace the boilerplate with lambdas, but that does not excuse omitting analysis of predicate semantics and iterator invalidation.
Pitfalls
- Objects still exist from the position returned by remove/remove_if to the old end, but their values must not be treated as a list of the deleted elements.
- Capturing by reference so that copies share state does not make a predicate that depends on call order correct; under parallel algorithms it may also introduce data races.
Run an example
Minimum C++20 · complete program · Download .cpp
#include <cassert>
#include <vector>
int main() {
std::vector<int> scores{8, 2, 9, 4, 7, 6};
const int minimum = 6;
const auto removed = std::erase_if(scores, [minimum](int score) {
return score < minimum;
});
assert(removed == 2);
assert((scores == std::vector<int>{8, 9, 7, 6}));
if (scores.size() >= 3) {
scores.erase(scores.begin() + 2);
}
assert((scores == std::vector<int>{8, 9, 6}));
}
Compile locally
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-more-exceptional-cpp.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If the requirement is to delete the third item of the original input and then delete values below 6, what is the result?
Show a reference answer
First perform erase(begin()+2) on the original sequence, deleting 9, yielding 8, 2, 4, 7, 6; then perform the same erase_if, deleting 2 and 4, finally 8, 7, 6. This differs from the example result, showing that position deletion and conditional filtering cannot be arbitrarily reordered.
Check the sources
- More Exceptional C++ Item 3 — Matters of State
- More Exceptional C++ Item 2 — What remove() Removes
- C++ draft [vector.erasure] — erase and erase_if
Drafts and official chapters change. The version mark is only the example’s minimum.