Concepts / Container Algorithms in C++

Container Algorithms in C++

The guidance is to prefer standard-library algorithms over code written in the bare language. Container algorithms are the part of that family that operates on containers such as vectors.

  • Programming

The Same Jobs Keep Coming Back

Almost every C++ program eventually needs to do something to a collection of values. It might add the values up, search for one of them, or rearrange them. A programmer can write a loop by hand for each of these jobs, using only the bare features of the language. The C++ standard library, however, already provides ready-made algorithms for many of these jobs. When both options are available, which one should you use, and why? This article works through that practical question, using a small example and tracing exactly what happens in each version.

Key point

The guidance is to prefer standard-library algorithms over code written in the bare language. Container algorithms are the part of that family that operates on containers such as vectors.

Summing a Vector by Hand

Consider a simple task: add up the numbers stored in a vector. The hand-written approach uses an index-based loop to visit each element in turn. It starts a running total at zero and adds each value to it. Trace the code below before you look at anything else. Notice which parts of the program you have to understand to be sure it is correct: the starting value, the loop condition, the index update, and the body that adds to the total.

cpp
Notes on the code
  • total starts at zero before the loop runs
  • i begins at 0 and the condition i < numbers.size() decides when to stop
  • ++i moves to the next position after each pass
  • total += numbers[i] adds the current element to the running total

What do you think happens?

Before reading further, predict what this program prints.

Reveal answer
Answer:

18

The loop visits 3, 5, 8, and 2 in order. The running total goes 0, 3, 8, 16, 18, so the program prints 18.

Replacing the Loop with a Library Call

The second version of the same task hands the repetitive work to a standard-library algorithm. The standard library's summing algorithm is std::accumulate, declared in the numeric header. Both versions compute the same total, but the second one is shorter and expresses the intent directly: accumulate the values from the beginning of the vector to its end, starting from zero.

Before
int total = 0;
for (std::size_t i = 0; i < numbers.size(); ++i) {
    total += numbers[i];
}

The programmer writes the start value, the loop condition, the index update, and the body by hand.

After
int total = std::accumulate(numbers.begin(), numbers.end(), 0);

The algorithm owns the traversal. The programmer supplies the range and the starting value.

Map the pieces of the raw loop onto the algorithm call. The starting value 0 in the call plays the role of the initialization total = 0. The pair numbers.begin() and numbers.end() replaces the index variable and the loop condition, because together they say where the traversal starts and where it stops. The update step and the body are handled inside the algorithm, so the reader no longer has to check the increment or the bounds each time. This particular task needs no predicate, since every element is simply added. Other algorithms accept a predicate, which is a rule that the algorithm applies to each element, and that is where a hand-written body would otherwise have to be rewritten.

cpp
Output
18
Notes on the code
  • The <numeric> header provides the summing algorithm
  • numbers.begin() and numbers.end() define the range to process
  • The final argument 0 is the starting value of the sum

Why the Library Version Is Preferred

The main reason to prefer the library version is that standard-library algorithms tend to be more carefully designed, specified, and implemented than the average hand-crafted loop. The author of a hand-written loop has to get every detail right in one place: the starting value, the boundary, the increment, and the update. A library algorithm has already been built and specified for that job, so the reader does not have to re-verify the loop mechanics each time the code is read. The code's intent becomes visible at a glance.

Iterator Ranges as Positions

The pair numbers.begin() and numbers.end() describes a range. You can read it as a half-open range, written $[\mathrm{begin}, \mathrm{end})$: the range includes the element at begin and stops just before end. The end position is one past the last element, so it marks the boundary rather than an element that gets processed. This is why the algorithm call in the previous section processes exactly the four values 3, 5, 8, and 2, and nothing beyond them.

beginstart of the range3element 05element 18element 22element 3endone past the last elementincludedstop before end
Which elements does a standard algorithm process when it is given begin and end for the vector 3, 5, 8, 2?

The diagram shows the four stored values as cells in order. The range starts at the first cell and runs through the last one. The end position sits after the final cell, so an algorithm given this pair processes every stored value and stops without touching anything beyond the vector.

Whole Containers Versus Iterator Pairs

Container algorithms are a category of algorithms that operate on containers, meaning the kind of thing they work on is a whole collection. Algorithms in the wider family can be given the same kind of input as a pair of positions instead. The two forms differ in what the caller hands over. The comparison below sets them side by side.

FeaturePassing an iterator pairPassing the whole container
Input the caller suppliesA begin position and an end positionThe container itself
Example callstd::accumulate(numbers.begin(), numbers.end(), 0)The container is passed directly in the ranges form introduced in C++20
What defines the rangeThe two positions chosen by the callerThe whole container, from its first element to its last
Good fitA sub-range, such as only part of a vectorProcessing every element of a collection

Two ways to tell an algorithm which elements to process

Where Container Algorithms Fit

The relationship between these ideas is hierarchical. Standard-library algorithms form the broader family that the guidance recommends. Container algorithms are one category within that family, defined by the kind of thing they operate on: containers. The preference for library algorithms over hand-crafted loops applies to the whole family, and the container category is one way of organizing the members of that family by their input.

Key point

Standard-library algorithms are the family. Container algorithms are the subset defined by working on containers.

Mistakes Beginners Make with Library Algorithms

  • Common mistake

    Assuming the end position is an element that gets processed

    What it looks like

    Calling an algorithm with numbers.end() and expecting the value after 2 to be included

    Why it fails

    The end position marks the boundary of the range. It is one past the last element and is not processed.

    Do this instead

    Read the range as beginning at the first position and stopping just before the end position

  • Common mistake

    Writing the same loop again out of habit when a standard algorithm matches the job

    Why it fails

    A hand-written loop makes the reader re-check the starting value, boundary, and update every time. The standard algorithm was designed and specified for exactly this job.

    Do this instead

    Check whether a standard-library algorithm already does the job before writing a loop by hand

  • Common mistake

    Forgetting the starting value in a summing call

    What it looks like

    std::accumulate(numbers.begin(), numbers.end()) with the initial value omitted

    Why it fails

    The starting value is what the total builds from. Without it the call does not express the zero-start of the hand-written loop.

    Do this instead

    Pass the starting value explicitly, as in std::accumulate(numbers.begin(), numbers.end(), 0)

Tracing a Loop Against a Library Call

Take the loop from the first section and ask what it would take to replace it. Identify the starting value, the range the loop covers, and any work the body does beyond adding each element. If the body only adds elements, a summing algorithm with the same range and starting value matches the job. If the body does something more specific, you have to decide whether a standard algorithm can express it or whether a loop is still the clearest choice.

EASY

A program stores the values 4, 7, 1 in a vector named scores. Write the single statement that adds them up using a standard-library algorithm, starting the total at zero. Then say which positions you passed as the range and why the end position is not processed.

Hints
  • The summing algorithm takes a begin position, an end position, and a starting value
  • The end position is one past the last element, so it is not included in the sum
  • The expected total is 12

Key Takeaways

  • Almost every C++ program needs to process collections, and the standard library already provides ready-made algorithms for many of those jobs.
  • Prefer standard-library algorithms over hand-written loops because they are carefully designed, specified, and implemented, and readers do not need to re-verify their loop mechanics.
  • A range given by begin and end is half-open: it includes the first position and stops before the end position, which is one past the last element.
  • Container algorithms are a category within the standard-library algorithm family that operates on containers, and they can be contrasted with algorithms given an iterator pair.

Related concepts

More concepts starting with C