Concepts / Using Standard-Library Algorithms Effectively

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.

  • Programming

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}?

  • true

  • false

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

Key point

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.

cpp
Output
has_negative({4, 7, -1, 9}) returns true
Notes on the code
  • 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}.

Iteration 0element 4, not negativeIteration 1element 7, not negativeIteration 2element -1, negativeReturntrueadvance iadvance istop early
At iteration k, which element is current, and what does the loop do before the next step?

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.

v[0]4v[1]7v[2]-1v[3]9lastboundary, not readnextnextnextone past final element
Which elements does the algorithm read between first and last, and why is last one past the final element?

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.

cpp
Output
has_negative({4, 7, -1, 9}) returns true
Notes on the code
  • v.begin() and v.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.

Callerstd::any_ofwalks the rangePredicatelambda x < 0call with range and predicatetest 4falsetest 7falsetest -1truereturn true
When the algorithm runs, how does it call the predicate for each element and use each result to decide what to do next?

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.

Loop purposeask what the loopaccomplishesSearch for an elementstd::findTest any or allelementsstd::any_ofor std::all_ofCombine into onevaluestd::accumulateTransform eachelementstd::transformshapeshapeshapeshapeexpressed byexpressed byexpressed byexpressed by
Which common loop shapes correspond to which named standard-library algorithms?

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.

cpp
Output
sum_of({3, 1, 4}) returns 8
Notes on the code
  • 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.

Before
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.

After
return std::accumulate(v.begin(), v.end(), 0);

The call names the combination and its starting value; the algorithm manages the walk.

Worked example

Following the running total through the algorithm

Compute std::accumulate over the vector {3, 1, 4} with initial value 0.

  1. 1
    Start

    The accumulated value is the initial value, 0. No element has been combined yet.

  2. 2
    Element 3

    The current element is 3. The accumulated value becomes 0 + 3 = 3.

  3. 3
    Element 1

    The current element is 1. The accumulated value becomes 3 + 1 = 4.

  4. 4
    Element 4

    The current element is 4. The accumulated value becomes 4 + 4 = 8.

  5. 5
    End of range

    The last position has been reached, so no further elements are combined and the result is returned.

Result

8

Mistakes When Replacing Loops

  • Common mistake

    Writing an algorithm call for a loop whose purpose you have not named

    Why it fails

    If you cannot state what the loop accomplishes in a short phrase, you have not yet recognized which algorithm, if any, expresses it.

    Do this instead

    Name 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 like
    std::accumulate(v.begin(), v.end(), 0) with an empty vector returns 0
    Why it fails

    The 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 instead

    Pass 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 fails

    The range covers elements from first up to but not including last, so last marks the boundary.

    Do this instead

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

    std::any_of stops as soon as the predicate returns true for one element

    Why it fails

    Like the hand-crafted loop with an early return, the algorithm can stop before the end of the range.

    Do this instead

    Describe 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.

DimensionHand-crafted loopStandard-library algorithm
Stated intentMust be reconstructed from the stepsNamed in the call, such as any_of or accumulate
Traversal and bookkeepingWritten by the author: start, advance, stopSupplied by the algorithm
Decisions the author must get rightStart, movement, stopping, and each actionThe predicate or operation passed in, plus the initial value where one applies
MaintenanceNext reader must check every decisionNext reader recognizes the named pattern

What changes when a hand-crafted loop is replaced by a standard-library algorithm

Best practice

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

MEDIUM

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.

Related concepts

More concepts starting with U