Best Practices for Using STL Algorithms
STL algorithms do not directly add or subtract elements from their argument sequences, so the sequence size is unchanged by the algorithm call alone.
Predicting the Size After a Removal
Almost every C++ program eventually needs to do something to a collection of values, such as adding them up, searching for one, or rearranging them. The C++ standard library already provides ready-made algorithms for many of these jobs, so a programmer can often choose between writing a loop by hand and calling a library algorithm. This article answers a practical question: when both options are available, which one should you use, and why?
Before reading any further, try a prediction. Imagine a vector holding five integers, where two of them are zeros. You call the removal algorithm to get rid of every zero. Ask yourself how many elements the vector holds after that call returns. Your first guess is probably a smaller number, and that guess is exactly the belief this article will correct.
What do you think happens?
A vector of five integers has two zeros. After a removal algorithm is called to remove every zero, how many elements does the vector hold?
Reveal answer
Five
The algorithm does not delete positions from the vector. It only rearranges the values inside the range it was given, so the vector still has five positions. The rest of this article explains how the values move and why the count stays the same.
The Belief That Algorithms Edit Lists
A very natural first guess about an STL algorithm is that it works like a tool that edits a list directly. You might expect that calling an algorithm on a sequence can make the sequence longer by adding elements, or shorter by removing them. The name of an algorithm that seems to remove things, such as one that removes matching values, invites exactly this belief.
The rule to hold onto is this: algorithms do not directly add or subtract elements from their argument sequences. The number of elements in a sequence is therefore not changed by an STL algorithm call alone. An algorithm works on the values inside the range it is given. It can move those values around and it can choose which of them to report, but it does not have the power to delete the positions that the range covers.
Tracing std::remove on a Vector
Consider a concrete run. The vector starts with the values 3, 0, 7, 0, and 5. The removal algorithm is asked to remove every value equal to zero. The algorithm does not look at the vector as a single object that can shrink. Instead, it walks through the range from the first position to the end, and every value it decides to keep is moved toward the front of the range. When it finishes, it returns an iterator that marks where the kept values end.
5
3- The vector is created with five positions, two of which hold zero.
- std::remove rearranges the values and returns the new logical end.
- The first size print shows the vector still has five positions after the algorithm.
- The erase call is a separate step that actually removes the trailing positions.
- The second size print shows the size after the sequence itself was changed.
v = {3, 0, 7, 0, 5}Five positions, two of them holding zero.
v = {3, 7, 5, ?, ?} // size still 5Five positions. The first three hold the kept values, and the last two hold values you should not rely on.
The diagram shows the two states side by side. Both sequences still have five positions, which is why the size is unchanged. The only thing that changed is the arrangement of the values: the kept values are gathered at the front, and the tail holds values your program should ignore. The algorithm did what it was designed to do, which is to rearrange and select values, and it did not change how many positions exist.
Reading the Range an Algorithm Works On
Every STL algorithm is given a range, usually as a pair of iterators: a begin position and an end position. The range covers the positions from begin up to, but not including, end. The algorithm reads and writes only the slots inside that range. It can write values into those slots, swap them, or move them, but the set of slots is fixed by the range it was handed.
Notice what this means for sizing. The five slots exist before the call and they exist after the call. The algorithm can change which value occupies each slot, but the number of slots is determined by the vector, not by the algorithm. That is why you should never read the size of a sequence as a count of how many values the algorithm kept.
Making the Sequence Actually Shorter
If you want the sequence itself to end up shorter, the change has to be made through the sequence, not through the algorithm. The usual pattern has two steps. First, call the algorithm and keep the iterator it returns as the new end of the useful values. Second, ask the sequence to erase everything from that point to its real end. The algorithm rearranges the values, and the erase call changes the size. Because these are two separate steps, each has a clear job.
- The first line rearranges the values and returns the position where the kept values end.
- The second line erases the positions from that point to the real end of the vector.
- Only after the erase call does the vector's size reflect the number of kept values.
Treat the call to an algorithm and the change to the sequence's size as two separate decisions. Read the returned iterator as the boundary of the useful values, and do not read past it. If the program needs the size to change, make that change explicitly on the sequence.
Mistakes With Sequence Size
Common mistake Assuming that calling a removal algorithm shrinks the sequence
What it looks likeAfter std::remove(v.begin(), v.end(), 0), printing v.size() still shows 5 for the vector {3, 0, 7, 0, 5}.
Why it failsAlgorithms do not directly add or subtract elements from their argument sequences. They only rearrange or select values inside the range they were given.
Do this insteadRead the returned iterator as the new logical end, then call erase from that iterator to v.end() if the size must change.
Common mistake Reading the size as the number of values the algorithm kept
What it looks likePrinting v.size() after the removal and reporting it as the count of non-zero values.
Why it failsThe size is the number of positions in the sequence, and those positions remain after the call. The count of kept values is indicated by the returned iterator, not by the size.
Do this insteadUse the returned iterator to find where the kept values end, and treat everything after it as not meaningful.
Common mistake Reading values in the tail after the returned end
Why it failsThe trailing positions still exist, but they do not hold a meaningful value for your program.
Do this insteadOnly use positions from begin up to the iterator the algorithm returned, or erase the tail before using the sequence further.
Why Library Algorithms Beat Hand-Written Loops
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. The loop starts a running total at zero and adds each value to it. This works, but the reader has to check the loop mechanics: the starting index, the stopping condition, and the update of the total.
- The running total starts at zero.
- The index runs from the first position to the last one.
- Each value is added to the total in turn.
15- The single call expresses the intent: sum the values in this range, starting from zero.
- The loop bookkeeping is handled inside the standard-library algorithm.
What do you think happens?
Before reading the full program, predict what it prints when the total is printed after the algorithm call on the values 1, 2, 3, 4, and 5.
Reveal answer
15
Both versions compute the same total, which is the sum of 1 through 5. The library version simply hands the repetitive work to a carefully designed and implemented algorithm.
The library version is preferred for a practical reason: the standard-library algorithm has been designed, specified, and implemented with care. Standard-library algorithms tend to be more carefully designed, specified, and implemented than the average hand-crafted loop. A reader does not have to re-verify the loop mechanics each time the code is read, because the call itself says what is being done.
Spotting a Loop an Algorithm Could Replace
When you read a hand-written loop, ask what job it is doing. If the loop is visiting every value to add them up, searching for one, or rearranging them, a standard-library algorithm probably already exists for that job. The guidance is to prefer standard-library algorithms over code written in the bare language. The loop that remains is often the sign that the programmer has not yet noticed the library call that matches the intent.
Container Algorithms as a Category
The relationship between these ideas is hierarchical. Standard-library algorithms are the broader family that the guidance recommends. Container algorithms form one category within that family, defined by the kind of thing they work on: containers. So a container algorithm is still a standard-library algorithm, and the category is a way of grouping algorithms by their target.
The same ideas apply inside any container category. Whatever the category, the sizing rule from earlier still holds: an algorithm that operates on a container rearranges or selects values and does not add or remove the container's elements on its own. Any change in size comes from an explicit operation on the container itself.
Predicting Sizes Before Running Code
A vector holds the values 4, 9, 4, 2, and 4. You call std::remove on the vector to remove every value equal to 4, and then you print v.size() right after the call returns. Predict the printed size, then state which values sit in the front part of the vector and which positions are in the tail.
Hints
- Algorithms rearrange values inside the range they are given. They do not delete positions.
- The kept values are 9 and 2, so they are gathered at the front in their original order.
- The size printed right after the call counts all positions, not only the kept values.
Extend the previous case: after the remove call, add the erase step so the vector holds only the kept values. What size does the vector report at the end?
Hints
- Keep the iterator returned by std::remove as the new end of the useful values.
- Erase from that iterator to
v.end().
Key Takeaways
- STL algorithms do not directly add or subtract elements from their argument sequences, so calling an algorithm alone leaves the sequence size unchanged.
- An algorithm such as std::remove rearranges or selects the values inside the range it is given and returns an iterator marking where the useful values end, leaving the trailing positions in place.
- To shrink a sequence, make the change through the sequence itself, such as by erasing the positions from the returned iterator to the real end.
- Prefer standard-library algorithms over hand-written loops when a library algorithm matches the job, because they are carefully designed, specified, and implemented.
- Container algorithms are a category of standard-library algorithms defined by the containers they operate on.