Concepts / Sequences in the C++ Standard Library

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.

  • Programming

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?

  • 3

  • 3.5

  • 4

  • 4.0

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

0starting value1after visiting 13after visiting 26final resultadd 1add 2add 3
After each element is visited, what is the running total, and how does it carry forward until the walk ends?

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.

cpp
Output
6
Notes on the code
  • values.begin() is the first element, the starting point of the walk
  • values.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
Worked example

Summing Three Integers

Find the value returned by accumulate() over the sequence {1, 2, 3} with starting value 0 and the default addition.

  1. 1
    Start

    The running total is the starting value, 0.

  2. 2
    Visit 1

    Combine the running total with 1 using addition: 0 + 1 = 1. The running total is now 1.

  3. 3
    Visit 2

    Combine the running total with 2: 1 + 2 = 3. The running total is now 3.

  4. 4
    Visit 3

    Combine the running total with 3: 3 + 3 = 6. The walk has visited every element, so the walk ends.

Result

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.

4index 0, begin8index 115index 2endone past the last elementvisitedvisitedstop
Which elements are included in the walk, and where do the bounds sit relative to the stored values?

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.

cpp
Output
24
Notes on the code
  • 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.

start 0intstart 0.0doublerunning total1.5 becomes 1, then 3.5becomes 3running totalfractions keptresult 3result 4
Why does the same pair of numbers produce a different total when the starting value is an int instead of a double?
cpp
Output
4
3
Notes on the code
  • 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 like

    accumulate over {1.5, 2.5} with starting value 0 returns 3

    Why it fails

    The running total takes the type of the starting value, so fractional parts are discarded along the way.

    Do this instead

    Start with 0.0 so that the running total is a double.

  • Common mistake

    Starting a product with 0

    What it looks like

    accumulate over {2, 3, 4} with multiplication and starting value 0 returns 0

    Why it fails

    Multiplying anything by 0 keeps the running total at 0, so no element can change the result.

    Do this instead

    Start 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 like
    accumulate(values.begin(), values.end() - 1, 0) over {4, 8, 15} returns 12
    Why it fails

    The range is half-open, so the element at the end bound is excluded, and the last element is never visited.

    Do this instead

    Pass values.end() when every element should be included.

  • Common mistake

    Leaving out the operation when a product is wanted

    What it looks like

    Calling accumulate over {2, 3, 4} with starting value 1 and no operation returns 10

    Why it fails

    Without a supplied operation, the default combining step is addition, so the walk computes a sum rather than a product.

    Do this instead

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

  1. 1
    Confirm the starting value and its type, because it sets the type of the running total.
  2. 2
    Confirm the beginning and end bounds, so you know exactly which elements are included.
  3. 3
    Confirm the operation, because a default sum and a custom operation can produce very different totals from the same elements.

Practicing a Running Total Trace

EASY

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.

Related concepts

More concepts starting with S