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.
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.
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.
- 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
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.
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.
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.
18- The <numeric> header provides the summing algorithm
numbers.begin()andnumbers.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.
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.
| Feature | Passing an iterator pair | Passing the whole container |
|---|---|---|
| Input the caller supplies | A begin position and an end position | The container itself |
| Example call | std::accumulate(numbers.begin(), numbers.end(), 0) | The container is passed directly in the ranges form introduced in C++20 |
| What defines the range | The two positions chosen by the caller | The whole container, from its first element to its last |
| Good fit | A sub-range, such as only part of a vector | Processing 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.
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 likeCalling an algorithm with numbers.end() and expecting the value after 2 to be included
Why it failsThe end position marks the boundary of the range. It is one past the last element and is not processed.
Do this insteadRead 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 failsA 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 insteadCheck 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 likestd::accumulate(numbers.begin(), numbers.end()) with the initial value omitted
Why it failsThe 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 insteadPass 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.
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.