Standard Algorithms and Containers
An algorithm does not need to know which kind of container it is working with, as long as the container provides the iterators the algorithm needs.
Reading the Test Before Its Explanation
Many programs are checked by small tests. A test runs a function on known input and looks for anything unexpected. The test in this article checks find_all(), a function that searches a string for every occurrence of one character. Before you read any explanation, read the test itself and notice what it does with each result it receives.
The test contains no list of expected output and no counting. It makes one promise: every position that find_all() reports should hold an 'a'. If that promise is broken even once, a message appears. The test is therefore about the quality of each individual result, not about how many results there are.
What do you think happens?
Work through the test with the text Mary had a little lamb in mind. Every position find_all() reports holds an 'a'. What does the test print?
Reveal answer
Nothing at all is printed.
The message appears only when a match does not hold the character being searched for. When every match holds an 'a', the loop finishes silently.
Why a Silent Run Still Means Success
A test that prints nothing is easy to misread. A beginner may think the test never ran, or that the function returned nothing at all. The way to tell the difference is to remember what the loop does. The loop runs once for each match, so a silent run still means that find_all() produced matches and that each of them was checked.
Silence from this test is a positive result. The absence of the message means that every match the loop visited passed the check. It does not mean that nothing happened.
Iterators as Positions in a Container
An iterator is a handle that refers to one place inside a container. The places in a C++ string are numbered from zero, so the first character sits at position 0, the next at position 1, and so on. An iterator tells you which place it is pointing to. It does not carry the letter itself. The letter is found by looking at the place the iterator refers to.
The distinction between the position and the value is the key to reading the test. The check does not ask where the iterator is. It asks what the iterator refers to. Two iterators can refer to different positions and still hold the same letter, and an iterator at position 1 is not the same thing as the character a.
The Loop Variable Visiting Each Match
The loop variable p starts at the first match that find_all() returns. Each time the loop body finishes, p moves on to the next match, and the loop body runs again with p pointing at that match. The loop stops when no matches remain. In this way the loop variable uses the iterators returned by find_all() to visit every match in turn.
Each iteration examines one result and only that result. The loop does not compare the matches with one another, and it does not keep a total. Its only job is to ask, for the current match, whether the value it refers to is an 'a'.
Tracing Mary Had a Little Lamb by Hand
To see the test succeed in detail, trace the string by hand. Count positions from zero. The table below shows the first six characters of the text and marks each one as accepted or rejected, along with the 'a' positions that appear later in the string. The full string has 22 characters, and the four 'a' positions found in it are 1, 6, 9 and 19.
| Position | Character | Would the test accept it as a match? |
|---|---|---|
| 0 | M | No, find_all() does not report it |
| 1 | a | Yes, reported and holds an 'a' |
| 2 | r | No, find_all() does not report it |
| 3 | y | No, find_all() does not report it |
| 4 | space | No, find_all() does not report it |
| 6 | a | Yes, reported and holds an 'a' |
Selected positions from Mary had a little lamb. The remaining 'a' positions are 9 and 19.
Predicting the result of the test on Mary had a little lamb
find_all() reports the positions of every 'a' in the string Mary had a little lamb. Predict whether the test prints anything.
- 1Get the first match
The loop starts with p referring to position 1, which holds an 'a'.
- 2Check the value
The if condition compares the value at p with 'a'. They are equal, so the message does not appear.
- 3Advance and repeat
p moves to position 6, then 9, then 19. Each of these holds an 'a', so the check passes each time.
- 4Finish
No match is left, so the loop ends without printing anything.
The test prints nothing. The silence means that all four matches were visited and each one holds an 'a'.
The four positions in this trace are a generated check of the source example rather than a fact the source states. Their value is that they show what a correct result looks like, so you have something to compare against when a result does not match.
Locating Where a Result Diverges
Now suppose find_all() were broken and reported a position that holds a different letter, such as the r at position 2. The loop would reach that result and the if condition would become true, so the message bug! would appear. The output alone tells you that a mismatch happened, but not which result caused it.
The divergence is at the single loop iteration where the character under p is not 'a'. In the broken example, that is the iteration whose match sits at position 2. Every earlier iteration stayed silent, and every later one would only be checked after the message had already been printed in that earlier iteration.
To pin the failure down, print the position or the character inside the loop as well. This turns a single error message into a record of every match that was checked. Once you can see the sequence, the first unexpected entry marks where the result diverged from what you expected.
Why find_all() Works Beyond Strings
So far find_all() has been used on a string, but the idea behind it is not tied to strings. The principle that makes this possible is that iterators and standard algorithms behave the same way on every standard container for which their use makes sense. An algorithm does not need to know which kind of container it is working with, as long as the container provides the iterators the algorithm needs.
Because the algorithm only asks for iterators, the same search can be written to work on other standard containers instead of searching only a string for a character. The test would still express the same expectation in that case: every match reported should satisfy the condition being searched for. The string and the other container types share the iterator interface, and that shared interface is what lets one generic find_all() operate on all of them.
The test checks the quality of each result, not the container it came from. This is why the same expectation can carry over to a different container without changing its meaning.
Mistakes Beginners Make When Reading the Test
Common mistake Concluding that a silent run means the test never ran
What it looks likeSeeing no output from the test and deciding that find_all() returned nothing.
Why it failsThe loop runs once for each match. Silence means the loop visited every match and each one passed the check.
Do this insteadRemember that the message is the only signal of failure, so no message means every visited result held an 'a'.
Common mistake Comparing the iterator instead of the value it refers to
What it looks likeReading the check as a test of where p is, rather than what p refers to.
Why it failsThe iterator is a position. The check examines the character at that position, which is why the test is about characters.
Do this insteadRead the dereferenced value at p and compare that with 'a'.
Common mistake Assuming a failure message identifies the bad match
What it looks likeSeeing bug! printed once and assuming it points at the last match.
Why it failsThe message only says that some iteration failed. It does not say which result caused it.
Do this insteadPrint the position or character inside the loop to find the first unexpected entry.
Common mistake Believing find_all() only works on strings
Predicting a Failure in a Changed Test
Suppose find_all() were broken and reported position 2 of Mary had a little lamb as a match, while still reporting the four 'a' positions. Predict which loop iteration prints the bug! message, and state the value that iteration's iterator refers to.
Hints
- Use the trace table. Which character sits at position 2?
- Position 2 is reported in a different order than the 'a' positions. Think about which iteration reaches it.
- The message appears on the first iteration where the value is not 'a'. The value at that iteration is the r.
Key Takeaways
- The
find_all()test loops over every match and reports a problem only when a match does not hold the character being searched for, so a passing run prints nothing. - Each loop iteration uses an iterator that refers to one position, and the check compares the value at that position, not the iterator itself.
- When a result diverges, the failure is at the first loop iteration whose value breaks the rule, and printing values at each step isolates it.
- Standard algorithms behave the same way on every standard container for which their use makes sense, which is why
find_all()can be generalized beyond strings.