Sequences in the C++ Standard Library
The generalized numerical algorithms in <numeric>, such as accumulate(), work on every standard-library sequence and can take an operation as an argument.
A Total That Comes Out Wrong
Suppose you have two prices, 1.5 and 2.5, stored in a sequence, and you want their total. You call the summing algorithm from the <numeric> header, but you pass the integer 0 as the starting value instead of 0.0. Before you read on, make a prediction about what gets printed. The answer depends on a detail that is easy to miss: the starting value and its type, not the numbers in the sequence, control the type of the result.
What do you think happens?
What total does accumulate() return for the sequence {1.5, 2.5} when the starting value is the integer 0?
Reveal answer
3
The running total takes the type of the starting value, which is int here. After the first step, 0 + 1.5 gives 1.5, which is stored as the int 1. Then 1 + 2.5 gives 3.5, which is stored as the int 3. The fractional parts are lost at each step, so the printed total is 3 rather than 4.
What the Numeric Algorithms Share
The <numeric> header provides a small set of generalized numerical algorithms. The one this article studies is accumulate(), which is the algorithm behind the sum calculations you will see in the code that follows.
The word generalized describes two properties. First, these algorithms are not tied to one kind of container. Each one is written to work on every kind of standard-library sequence, so the same accumulate() call pattern applies whether the values live in a vector, a list, or an array. Second, the operation applied to the elements is itself a parameter that the caller can supply. The algorithm is written once, and the sequence and the operation are provided by whoever calls it.
Each algorithm also comes in two forms. The general form takes an operation as an argument. The common form applies the most common operator for that algorithm. For accumulate(), the common form computes a plain sum, which is the everyday case. The general form lets you replace that sum with any combining step you choose. Keeping both forms in mind helps explain why the same walk through a sequence can produce very different results.
Tracing a Running Total Step by Step
accumulate() takes three required arguments in its basic form: the beginning of the sequence, the end of the sequence, and a starting value. It begins with the starting value as its running total. It then visits each element from the beginning to the end and combines the running total with that element. When the walk has visited every element, the running total is the result. Watch three things as the walk runs: which value is the starting point, which elements are visited and in what order, and what number finally reaches the output.
The following program sums a vector of three integers. The starting value is 0, so the running total begins at zero and grows by each element in turn.
6values.begin()is the first element, the starting point of the walkvalues.end()is one past the last element, so the walk stops after 3- 0 is the starting value and the initial running total
- The returned total is stored in s and printed
Summing Three Integers
Find the value returned by accumulate() over the sequence {1, 2, 3} with starting value 0 and the default addition.
- 1Start
The running total is the starting value, 0.
- 2Visit 1
Combine the running total with 1 using addition: 0 + 1 = 1. The running total is now 1.
- 3Visit 2
Combine the running total with 2: 1 + 2 = 3. The running total is now 3.
- 4Visit 3
Combine the running total with 3: 3 + 3 = 6. The walk has visited every element, so the walk ends.
accumulate() returns 6, which is printed.
Reading the Range from Begin to End
The two iterator arguments define a range written $[first, last)$. The range includes the element at first and every element after it, up to but not including the element at last. Since the end iterator marks a position one past the final element, the walk never touches it. That is why values.end() is the correct second argument when you want every element of a vector.
Consider the vector {4, 8, 15} with starting value 0. The call accumulate(values.begin(), values.end(), 0) visits all three values and returns 27. The same walk with the end bound pulled back by one element would return 12, because 15 would never be visited. Changing a bound does not change the algorithm at all, only which stored values it reaches.
Swapping the Combining Step
The default form combines the running total with each element by addition. A supplied operation changes only that combining step. The walk itself, from the starting value through each element in order to the final running total, stays exactly the same. This means you can compute a product by passing a multiplication operation as the fourth argument, with 1 as the starting value, because 1 is the identity for multiplication.
24- std::multiplies<int>() is the supplied operation that replaces addition
- 1 is the starting value, the identity for multiplication
- Each step computes running total times element: 1 * 2, then 2 * 3, then 6 * 4
How the Starting Value Sets the Type
The starting value is the first running total, so its type shapes every combining step that follows. When the starting value is an integer, the running total is an integer, and each step's result is held in that integer form. Fractions produced by the elements are then lost. The diagram below compares the same sequence {1.5, 2.5} with an int starting value and a double starting value.
4
3- 0.0 starts the running total as a double, so fractions survive every step
- 0 starts the running total as an int, so 1.5 is truncated to 1 and 3.5 to 3
- The elements are the same in both calls; only the starting value differs
Mistakes That Produce Wrong Totals
Common mistake Using an integer starting value for a sequence of fractions
What it looks likeaccumulate over {1.5, 2.5} with starting value 0 returns 3
Why it failsThe running total takes the type of the starting value, so fractional parts are discarded along the way.
Do this insteadStart with 0.0 so that the running total is a double.
Common mistake Starting a product with 0
What it looks likeaccumulate over {2, 3, 4} with multiplication and starting value 0 returns 0
Why it failsMultiplying anything by 0 keeps the running total at 0, so no element can change the result.
Do this insteadStart a product with 1, the identity for multiplication, which gives 24 for {2, 3, 4}.
Common mistake Passing the wrong end bound
What it looks likeaccumulate(values.begin(), values.end() - 1, 0) over {4, 8, 15} returns 12Why it failsThe range is half-open, so the element at the end bound is excluded, and the last element is never visited.
Do this insteadPass values.end() when every element should be included.
Common mistake Leaving out the operation when a product is wanted
What it looks likeCalling accumulate over {2, 3, 4} with starting value 1 and no operation returns 10
Why it failsWithout a supplied operation, the default combining step is addition, so the walk computes a sum rather than a product.
Do this insteadPass std::multiplies<int>() as the fourth argument.
Diagnosing an Unexpected Result
Most wrong results from accumulate() come from its inputs rather than from the algorithm itself. When a printed total does not match your hand calculation, follow the same order as the walk.
- 1Confirm the starting value and its type, because it sets the type of the running total.
- 2Confirm the beginning and end bounds, so you know exactly which elements are included.
- 3Confirm the operation, because a default sum and a custom operation can produce very different totals from the same elements.
Practicing a Running Total Trace
A vector holds the values {3, 1, 4}. Call accumulate() over the whole vector with starting value 2 and the default addition. Write down the running total after each element is visited, then state the value returned.
Hints
- Start with the running total equal to 2.
- Add the first element, 3, and record the new running total before moving on.
- Keep the order of the walk: the elements are visited from the beginning to the end.
Key Takeaways
- The <numeric> algorithms such as
accumulate()are written once and work on every standard-library sequence, with the combining operation supplied as an argument. accumulate()starts from the starting value, visits each element from begin to end, combines the running total with each element, and returns the final running total.- The range is half-open: the begin element is included and the end position is not visited, so
end()is the bound that covers every element. - A supplied operation changes only the combining step, which lets the same walk compute a product when the starting value is 1.
- When a result looks wrong, check the starting value and its type, then the sequence bounds, then the operation, in that order.