Concepts / Standard Algorithms and Containers

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.

  • Programming

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?

  • Every matching position, one per line

  • Nothing at all

  • A count of how many matches were found

  • An error about iterators

Reveal answer
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.

Key point

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.

Position 0MPosition 1aIterator prefers to a positionPosition 2rPosition 3yPosition 4spacerefers to
How does an iterator map to a specific position in the container, and how is that position different from the value stored there? The iterator p refers to position 1, and the value at that position is the letter a.

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.

Match 1position 1, holds aMatch 2position 6, holds aMatch 3position 9, holds aMatch 4position 19, holds anext iteratornext iteratornext iterator
How does the loop variable move from one match to the next, and what does each iteration's iterator point to in the original container? For Mary had a little lamb, the four matches sit at positions 1, 6, 9 and 19.

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.

PositionCharacterWould the test accept it as a match?
0MNo, find_all() does not report it
1aYes, reported and holds an 'a'
2rNo, find_all() does not report it
3yNo, find_all() does not report it
4spaceNo, find_all() does not report it
6aYes, reported and holds an 'a'

Selected positions from Mary had a little lamb. The remaining 'a' positions are 9 and 19.

Worked example

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.

  1. 1
    Get the first match

    The loop starts with p referring to position 1, which holds an 'a'.

  2. 2
    Check the value

    The if condition compares the value at p with 'a'. They are equal, so the message does not appear.

  3. 3
    Advance and repeat

    p moves to position 6, then 9, then 19. Each of these holds an 'a', so the check passes each time.

  4. 4
    Finish

    No match is left, so the loop ends without printing anything.

Result

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.

Next match fromfind_all()Does the value at pequal 'a'?Stay silent andcontinuePrint the bug!messageyesnoloop again
Which check in the test decides between staying silent and reporting a problem, and at which step does a bad match trigger the report? The check runs for each match in turn, and the first match whose value is not 'a' triggers the message.

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.

Best practice

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.

Key point

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 like

    Seeing no output from the test and deciding that find_all() returned nothing.

    Why it fails

    The loop runs once for each match. Silence means the loop visited every match and each one passed the check.

    Do this instead

    Remember 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 like

    Reading the check as a test of where p is, rather than what p refers to.

    Why it fails

    The iterator is a position. The check examines the character at that position, which is why the test is about characters.

    Do this instead

    Read the dereferenced value at p and compare that with 'a'.

  • Common mistake

    Assuming a failure message identifies the bad match

    What it looks like

    Seeing bug! printed once and assuming it points at the last match.

    Why it fails

    The message only says that some iteration failed. It does not say which result caused it.

    Do this instead

    Print 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

MEDIUM

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.

Related concepts

More concepts starting with S