Using Standard-Library Algorithms Effectively
Algorithms move the traversal and bookkeeping out of your code, so the code states the task directly. A reader of an algorithm call can see the purpose immediately, while a reader of a raw loop must reconstruct the purpose from the steps.
A Loop Hiding a Common Job
Almost every program that processes a collection of data contains a loop. A loop walks through the elements, checks something, accumulates a result, or stops early. Because loops are so familiar, it is easy to write one every time, even when the job it performs is a common one. This article builds a habit that experienced programmers rely on: before writing a hand-crafted loop, ask whether the loop could be expressed as a general algorithm from the standard library instead.
What do you think happens?
Consider a function that returns true if a vector of integers contains any negative number. What does it return for the vector {4, 7, -1, 9}?
Reveal answer
true
The vector contains -1, which is negative, so at least one element satisfies the condition and the answer is true. The rest of the article shows how a loop and a standard-library algorithm each arrive at that answer.
What a Hand-Crafted Loop Decides for You
A hand-crafted loop is code you write from scratch to walk through a sequence step by step. You decide how the loop starts, how it moves from one element to the next, when it stops, and what it does with each element. Every one of those decisions is an opportunity for a small mistake, and every one of them has to be read and checked by the next person who maintains the code.
A general algorithm works differently. It packages a common pattern of work under a name. When you call one, you state what you want done, and the algorithm supplies the walking, the stopping, and the bookkeeping. The phrase to hold onto is general algorithm: the same kind of task appears again and again across many programs, so a single well-known implementation can serve all of them.
Algorithms move the traversal and bookkeeping out of your code, so the code states the task directly. A reader of an algorithm call can see the purpose immediately, while a reader of a raw loop must reconstruct the purpose from the steps.
Tracing the Search for a Negative Value
Consider a function that answers one question: does a vector of integers contain any negative number? The first version uses a hand-crafted loop. It walks the elements in order, returns true as soon as it finds a negative value, and returns false if the loop finishes without finding one. The example below is an original teaching example written for this article.
has_negative({4, 7, -1, 9}) returns true- The index i is the position of the element being examined
- The if statement is the test applied to each element
- The early return is the stopping rule once a negative is found
Watch what the computation holds at each step. The loop has three pieces of state: the position i, the element currently being examined, and the answer that will be returned. The diagram follows those pieces for the vector {4, 7, -1, 9}.
The loop never examines the element 9. Once it sees -1 it returns, so the remaining elements are not visited. This early stop is part of the behavior you must write and check yourself in a hand-crafted loop.
Where the Range Starts and Stops
Most standard-library algorithms do not take a whole container. They take a range described by two positions: a first position, which is included, and a last position, which is excluded. The range therefore covers the elements from first up to but not including last. The last position usually sits one past the final element, so it is a boundary marker rather than an element the algorithm reads.
Reading this as a range is the reason many algorithm calls take a pair of positions where a loop would take a start index and a loop condition. The pair states the boundary once, and the algorithm applies it everywhere. An empty range is simply one where first and last are the same position, so the algorithm has nothing to read.
How the Algorithm Calls Your Predicate
The standard library provides std::any_of, which takes a range and a predicate and reports whether at least one element satisfies the predicate. A predicate is a callable you supply, here a small lambda that returns true for a negative value. The rewritten version states the purpose directly, and the algorithm supplies the walking and stopping that the loop had to spell out.
has_negative({4, 7, -1, 9}) returns truev.begin()andv.end()give the half-open range covering every element- The lambda is the predicate that tests one element at a time
- No index variable or early return appears in the caller
The control flow is what makes the algorithm work. Your code does not run a loop; it hands the algorithm a rule. The algorithm walks the range, calls the predicate on each element, and uses each result to decide whether to continue. The sequence below shows the exchange for the same vector.
Recognizing Loop Shapes
The practical skill is recognition. Many loops follow one of a small number of shapes: they search for an element, they test whether any or all elements satisfy a condition, they combine every element into one value, or they transform each element into something else. Reading the loop body and asking what the loop is trying to accomplish, rather than how it steps through the data, is the habit that makes this recognition possible.
When you see a loop with one of these shapes, consider whether a standard-library algorithm already expresses it. The question to ask is short: what is this loop for? If you can name that purpose in a short phrase, the phrase usually points to the algorithm. A loop that is described as finding the first negative value, or as totalling the prices, has already told you which algorithm to look for.
Tracing a Running Total
The second common shape is combining every element of a sequence into a single value. The loop below adds up the elements of a vector. The idea is familiar, so the loop is easy to read, but it still contains the bookkeeping of a running total that must be set up before the loop begins.
sum_of({3, 1, 4}) returns 8- total is the accumulated value, set up before the loop
- The loop body combines the current element into total
- The index i is bookkeeping the caller never needs to see
The same computation can be stated as a call to std::accumulate, which lives in the numeric header. It takes a range and an initial value and returns the combined result. The initial value of zero plays the role of the variable set up before the loop.
int total = 0;
for (std::size_t i = 0; i < v.size(); ++i) {
total = total + v[i];
}
return total;The caller manages total, the index i, and the loop bounds.
return std::accumulate(v.begin(), v.end(), 0);The call names the combination and its starting value; the algorithm manages the walk.
Following the running total through the algorithm
Compute std::accumulate over the vector {3, 1, 4} with initial value 0.
- 1Start
The accumulated value is the initial value, 0. No element has been combined yet.
- 2Element 3
The current element is 3. The accumulated value becomes 0 + 3 = 3.
- 3Element 1
The current element is 1. The accumulated value becomes 3 + 1 = 4.
- 4Element 4
The current element is 4. The accumulated value becomes 4 + 4 = 8.
- 5End of range
The last position has been reached, so no further elements are combined and the result is returned.
8
Mistakes When Replacing Loops
Common mistake Writing an algorithm call for a loop whose purpose you have not named
Why it failsIf you cannot state what the loop accomplishes in a short phrase, you have not yet recognized which algorithm, if any, expresses it.
Do this insteadName the purpose first, such as finding the first negative value or totalling the prices, and then look for the algorithm that matches.
Common mistake Forgetting the initial value when using std::accumulate
What it looks likestd::accumulate(v.begin(), v.end(), 0) with an empty vector returns 0Why it failsThe result of accumulate starts from the initial value, so the initial value determines what an empty range produces and what type the result has.
Do this insteadPass an initial value of the type and meaning you want, and check what the call returns for an empty range.
Common mistake Treating the last position as an element the algorithm reads
Why it failsThe range covers elements from first up to but not including last, so last marks the boundary.
Do this insteadRead the pair as a half-open range. Use v.end() as the boundary, not the position of the final element.
Common mistake Assuming the algorithm visits every element in every case
What it looks likestd::any_of stops as soon as the predicate returns true for one element
Why it failsLike the hand-crafted loop with an early return, the algorithm can stop before the end of the range.
Do this insteadDescribe what the algorithm does in terms of its purpose, and do not rely on it visiting elements you have not yet tested.
Intent, Correctness, and Readability
The two styles can be compared along several dimensions. The comparison is about readability and the amount of code the author must get right, not about any claim regarding speed.
| Dimension | Hand-crafted loop | Standard-library algorithm |
|---|---|---|
| Stated intent | Must be reconstructed from the steps | Named in the call, such as any_of or accumulate |
| Traversal and bookkeeping | Written by the author: start, advance, stop | Supplied by the algorithm |
| Decisions the author must get right | Start, movement, stopping, and each action | The predicate or operation passed in, plus the initial value where one applies |
| Maintenance | Next reader must check every decision | Next reader recognizes the named pattern |
What changes when a hand-crafted loop is replaced by a standard-library algorithm
Before writing a loop, name its purpose in one phrase. If the phrase matches a search, a test over all or any elements, a combination into one value, or a transformation of each element, check the standard library first. Fall back to a hand-crafted loop when no algorithm expresses the task clearly.
Rewriting a Loop Yourself
The function below counts how many elements of a vector are greater than 10 using a hand-crafted loop. Name the purpose of the loop, identify the general algorithm that expresses it, and rewrite the function as a single call. Then trace the state of the computation for the vector {12, 5, 20} at each step.
Hints
- Ask what the loop is for in one phrase. It is counting elements that satisfy a condition.
- Think about which algorithm counts elements matching a predicate, and what predicate you would pass.
- For the trace, record the current element, whether it passes the test, and the running count at each step.
Key Takeaways
- Hand-crafted loops require you to decide the start, the traversal, the stopping rule, and the action for each element, and each decision must be checked by every later reader.
- A general algorithm packages a common pattern under a name, so the call states the task and the algorithm supplies the walking, stopping, and bookkeeping.
- Most loops fit a small set of shapes: search, test any or all, combine into one value, or transform each element. Naming the loop's purpose reveals which algorithm already expresses it.
- Standard-library algorithms work on ranges bounded by a first position and a last position that is excluded, and they call your predicate or operation on each element, using the results to decide what to do next.
- When you replace a loop, check the initial value and the predicate, since these decide the result, and prefer the algorithm when the intent is clearer.