Using Predicates with Algorithms
When the action is supplied from outside, the algorithm keeps the same loop and stopping logic. Only the test at the decision point changes.
A Search Rule That Changes Between Calls
Imagine a warehouse inventory listed as item names paired with stock counts. A manager asks, "Which is the first item with more than 42 units on hand?" Answering that question means checking entries one at a time until one of them meets a rule. The rule here is not a plain equality check, as it would be if you were looking for one exact count. It is a condition that changes from one question to the next.
This article looks at how that condition can be handed to an algorithm as an input, so that the same search routine works for many different rules. The inventory picture stays in mind throughout. Each section returns to a concrete search and follows what happens to each element as it is examined.
What do you think happens?
If a search routine always looks for a count above 42, what would have to change in the routine to make it look for a count above 100 instead?
Reveal answer
Change only the rule being applied
The loop that visits elements and stops on a match stays the same. Only the test at the decision point depends on the threshold, so the rule is the only part that needs to change.
Why a Fixed Action Falls Short
Some algorithms have a fixed action built into their logic. They always do the same thing to each element they visit. That works well when the task never changes. In many practical cases, however, the task differs from one call to another. The routine that searches for a count above 42 is the same routine that would search for a count above 100, and the only difference is the rule being applied.
Writing a separate search routine for each rule would duplicate the same looping logic again and again. Every copy would visit elements in the same order, stop at the same kind of point, and differ only in one comparison. The effort of maintaining that duplicated looping logic grows with every new rule.
Predicates as Supplied Requirements
A predicate is a specified requirement or condition that an element must fulfill. An algorithm can accept it as a parameter instead of building the action in. The predicate answers a yes or no question about each element it is given.
The alternative to a fixed action is to treat the requirement as something the caller supplies. The algorithm keeps the looping and visiting logic, and the caller provides the piece that decides what each element must satisfy. That supplied piece is the one that varies between calls, which is what makes the algorithm reusable.
| Aspect | Action built into the algorithm | Action supplied as a predicate |
|---|---|---|
| Who decides the test | The algorithm, fixed in its own logic | The caller, passed in as a parameter |
| Loop and visiting logic | Lives inside the algorithm | Stays inside the algorithm, unchanged |
| What changes between calls | Nothing; the same action every time | Only the requirement at the decision point |
| Reusability | Works only for the one task built in | One routine serves many different rules |
Where the decision lives in a fixed action versus a supplied predicate
When the action is supplied from outside, the algorithm keeps the same loop and stopping logic. Only the test at the decision point changes.
The practical benefit is that the caller can change the search rule without touching the loop. Replacing the threshold, or replacing the whole test with a different condition, changes only the question asked at the decision point. The surrounding machinery of visiting elements in order and stopping on a match is reused unchanged.
Searching a Map of Pairs
A map stores associations between keys and values. To search a map with a predicate, we need to know what the algorithm actually sees as its elements. A map gives access to its contents as a sequence of (key, value) pairs. In C++ terms, a map<string,int> presents its elements as pair<const string,int> objects. The key is the string part, which the map treats as fixed once an entry exists, and the value is the int part.
This structure matters when writing a predicate. The test does not receive a bare integer when searching a map of this kind. It receives a whole pair, so the condition must look at the value portion of that pair to decide whether it qualifies. A predicate that ignores this structure will not behave as the reader expects.
it refers to the pair ("nails", 58)find_ifis the search algorithm; it visits the map's pairs in turn- The lambda is the supplied predicate; it returns true or false for each pair
- entry.second is the int value portion of the pair, so the test looks there
The search in the diagram takes one pair at a time and hands it to the predicate. The first pair fails the test, so the search advances. The second pair passes, so the search stops there and the third pair is never examined. The predicate is the only part that knows the threshold of 42. The traversal itself stays the same no matter what condition is supplied.
The control flow follows a predictable pattern. The algorithm starts at the beginning of the sequence and asks the supplied requirement about the current element. A yes answer ends the search at that element. A no answer advances to the next element and repeats the question. The flowchart later in this article traces this same cycle for a different search.
What find Hands Back
The find algorithm returns an iterator to the first matching element, or end() when the value is not in the range.
The return value is always an iterator, never a plain character or a yes-or-no flag. Because the result is an iterator, the caller has to decide what it means.
When find locates a match, the iterator points at the first matching position, which is the first l in hello here. When nothing matches, the iterator equals end(), the position just past the last element. In the diagram, end() is shown after the final character, because it marks the boundary of the range rather than a real element.
The source describes this as a convention shared by many standard-library search algorithms: they return end() to signal that nothing was found. Once you recognize that convention, you can apply the same reasoning to other searches as well.
Testing the Result Against end()
The decision is a single comparison. The variable holding the returned iterator is compared with s.end(). If the two differ, find stopped at a real matching position. If they are the same, find ran out of range without a match. The comparison does not inspect the character itself, so the same test works for any element type that find can search, not only characters.
Writing has_c with find
Suppose you need a small yes-or-no function: given a string and a character, report whether that character appears anywhere in the string. You could write a loop yourself, but the standard library already offers a search algorithm that does the scanning for you. The function below uses find so you can see the behavior, and the explanation follows from what find returns.
has_c("hello", 'l') returns trues.begin()ands.end()define the full range that find searches- p stores whatever iterator find returned
- The comparison p !=
s.end()is the entire decision
The same behavior can be written more compactly by returning the comparison directly. The two versions perform the same search and the same comparison, so they return the same result for every input.
has_c("hello", 'l') returns true- The inline comparison replaces the named variable and the if statement
- The result is identical to the longer version
What do you think happens?
What does has_c("hello", 'z') return?
Reveal answer
false
No character in hello equals z, so find runs past the last element and returns end(). The comparison p != s.end() is then false, and the function returns false.
Running the same function with 'c' instead of 'z' takes a different path. The search would stop at the first character, the comparison would find a real position, and the function would return true. The difference between the two cases lies entirely in what find returned.
Mistakes With Predicates and Results
Common mistake Testing a map's element as if it were a bare integer
What it looks likeWriting a predicate that compares the whole pair with 42 instead of its value part.
Why it failsThe test receives a pair<const string,int>, not an int, so the condition must look at the value portion of that pair.
Do this insteadRead the int part of the pair, for example entry.second > 42, before applying the requirement.
Common mistake Comparing the result of find against begin()
What it looks likeChecking if (p != s.begin()) to decide whether the character was found.
Why it failsA match can sit at the first position, so comparing against
begin()does not reflect whether a valid match exists.Do this insteadCompare the returned iterator against end(), which is the signal that nothing was found.
Common mistake Dereferencing end()
What it looks likeReading *p when p equals s.end() to check the character.
Why it failsend()marks the position past the last element, so it does not refer to a real element to read.Do this insteadTest p != s.end() first, and only dereference p after that test succeeds.
Common mistake Assuming find returns a character or a bool instead of an iterator
Practice: Checking for a Character
Write a function has_char that takes a std::string and a char, and returns true if the character appears anywhere in the string. Use std::find with s.begin() and s.end(), and decide the result by comparing the returned iterator against s.end(). Then predict what has_char("lantern", 'n') and has_char("lantern", 'x') return before checking your answer.
Hints
- find returns an iterator, not a bool, so you need a comparison to produce the answer
- A match returns an iterator that differs from
s.end() - A miss returns
s.end(), so the comparison should be false
Using a std::map<std::string,int> of stock counts, write a predicate lambda and a std::find_if call that locates the first entry whose count is greater than 100. State what the returned iterator equals when no entry qualifies.
Hints
- The lambda receives a pair, so use entry.second for the int
- When no entry qualifies, the returned iterator equals the map's
end()
Key Takeaways
- A predicate is a requirement or condition that an element must fulfill, and an algorithm can accept it as a parameter instead of building the action in.
- When the action is supplied, the algorithm keeps the same loop and stopping logic, and only the test at the decision point changes.
- A search over a map passes each (key, value) pair to the predicate, so the condition must examine the value portion, and the search usually stops at the first qualifying element.
- find returns an iterator to the first match or
end()when nothing matches, so the caller decides the result by comparing that iterator withend(). - A function such as has_c can check for a character by comparing the result of
find(s.begin(), s.end(), c)withs.end().