Using library algorithms
6.1 Analyzing strings
6.3.2 A single-pass solution
Our first algorithmic solution performs pretty well, but we should be able to do slightly better. The reason is that the solution in §6.3.1/117 calculates the grade for every element in students twice:
once from remove_copy_if and a second time from remove_if.
Although there is no library algorithm that does exactly what we want, there is one that approaches our problem from a different angle: It takes a sequence and rearranges its elements so that the ones that satisfy a predicate precede the ones that do not satisfy it.
There are really two versions of this algorithm, which are named partition and stable_partition. The difference is that partition might rearrange the elements within each category, and stable_partition keeps them in the same order aside from the partitioning. So, for example, if the student names were already in alphabetical order, and we wanted to keep them that way within each category, we would need to use stable_partition rather than partition.
Each of these algorithms returns an iterator that represents the first element of the second section.
Therefore, we can extract the failing grades this way:
vector<Student_info>
extract_fails(vector<Student_info>& students) {
vector<Student_info>::iterator iter =
stable_partition(students.begin(), students.end(), pgrade);
vector<Student_info> fail(iter, students.end());
students.erase(iter, students.end());
return fail;
}
To understand what is going on here, let's start with our hypothetical input data again:
After calling stable_partition, we would have
We construct fail from a copy of the failing records, which are the ones in the range [iter, students.end())., and then erase those elements from students.
When we ran our algorithm-based solutions, they had roughly the same overall performance as the list-based solution. As expected, once the input was large enough, the algorithm and list-based solutions were substantially better than the vector solution that used erase. The two algorithmic solutions are good enough that the time consumed by the input library dominated the timings for input files up to about 75,000 records. To compare the effects of the two strategies in extract_fails, we separately analyzed the performance of just this portion of the program. Our timings confirmed that the one-pass algorithm ran about twice as fast as the two-pass solution.
6.4 Algorithms, containers, and iterators
There is a fact that is crucial to understand in using algorithms, iterators, and containers:
Algorithms act on container elements—they do not act on containers.
The sort, remove_if, and partition functions all move elements to new positions in the underlying container, but they do not change the properties of the container itself. For
example, remove_if does not change the size of the container on which it operates; it merely copies elements around within the container.
This distinction is especially important in understanding how algorithms interact with the containers that they use for output. Let's look in more detail at our use of remove_if in
§6.3.1/117. As we've seen, the call
remove_if(students.begin(), students.end(), fgrade)
did not change the size of students. Rather, it copied each element for which the predicate was false to the beginning of students, and left the rest of the elements alone. When we need to shorten the vector to discard those elements, we must do so ourselves. In our example, we said
students.erase(remove_if(students.begin(), students.end(), fgrade), students.end());
Here, erase changes the vector by removing the sequence indicated by its arguments. This call to erase shortens students so that it contains only the elements we want. Note that erase must be a member of vector, because it acts directly on the container, not just on its
elements.
Similarly, it is important to be aware of the interaction between iterators and algorithms, and between iterators and container operations. We've already seen, in §5.3/83 and §5.5.1/86, that container operations such as erase and insert invalidate the iterator for the element erased. More important, in the case of vectors and strings, operations such as erase or insert also invalidate any iterator denoting elements after the one erased or inserted.
Because these operations can invalidate iterators, we must be careful about saving iterator values if we are using these operations.
Similarly, functions such as partition or remove_if, which can move elements around within the container, will change which element is denoted by particular iterators. After running one of these functions, we cannot rely on an iterator continuing to denote a specific element.
6.5 Details
Type modifiers:
static type variable;
For local declarations, declares variable with static storage class. The value of variable persists across executions of this scope and is guaranteed to be initialized before the variable is used for the first time. When the program exits from the scope, the variable keeps its value until the next time the program enters that scope. We'll see in §13.4/244 that the meaning of static varies with context.
Types: The built-in type void can be used in a restricted number of ways, one of which is to indicate that a function yields no return value. Such functions can be exited through a return; that has no value or by falling off the end of the function.
Iterator adaptors are functions that yield iterators. The most common are the adaptors that generate insert_iterators, which are iterators that grow the associated container dynamically.
Such iterators can be used safely as the destination of a copying algorithm. They are defined in header <iterator>:
back_inserter(c)
Yields an iterator on the container c that appends elements to c. The container must support push_back, which the list, vector, and the string types all do.
front_inserter(c)
Like back_inserter, but inserts at the front of the container. The container must support push_front, which list does, but string and vector do not.
inserter(c, it)
Like back_inserter, but inserts elements before the iterator it.
Algorithms: Unless otherwise indicated, <algorithm> defines these algorithms:
accumulate(b, e, t)
Creates a local variable and initializes it to a copy of t (with the same type as t, which means that the type of t is crucially important to the behavior of accumulate), adds each element in the range [b, e) to the variable, and returns a copy of the variable as its result. Defined in
<numeric>. find(b, e, t)
find_if(b, e, p) search(b, e, b2, e2)
Algorithms to look for a given value in the sequence [b, e). The find algorithm looks for the value t; the find_if algorithm tests each element against the predicate p; the search algorithm looks for the sequence denoted by [b2, e2).
copy(b, e, d)
remove_copy(b, e, d, t) remove_copy_if(b, e, d, p)
Algorithms to copy the sequence from [b, e) to the destination denoted by d. The copy algorithm copies the entire sequence; remove_copy copies all elements not equal to t; and remove_copy_if copies all elements for which the predicate p fails.
remove_if(b, e, p)
Arranges the container so that the elements in the range [b, e) for which the predicate p is false are at the front of the range. Returns an iterator denoting one past the range of these
"unremoved" elements.
remove(b, e, t)
Like remove_if, but tests which elements to keep against the value t. transform(b, e, d, f)
Runs the function f on the elements in the range [b, e), storing the result of f in d. partition(b, e, p)stable_partition(b, e, p)
Partitions the elements in the range [b, e), based on the predicate p, so that elements for which the predicate is true are at the front of the container. Returns an iterator to the first element for which the predicate is false, or e if the predicate is true for all elements. The stable_partition function maintains the input order among the elements in each partition.
Exercises
6-0. Compile, execute, and test the programs in this chapter.
6-1. Reimplement the frame and hcat operations from §5.8.1/93 and §5.8.3/94 to use iterators.
6-2. Write a program to test the find_urls function.
6-3. What does this program fragment do?
vector<int> u(10, 100);
vector<int> v;
copy(u.begin(), u.end(), v.begin());
Write a program that contains this fragment, and compile and execute it.
6-4. Correct the program you wrote in the previous exercise to copy from u into v. There are at least two possible ways to correct the program. Implement both, and describe the relative advantages and disadvantages of each approach.
6-5. Write an analysis function to call optimistic_median.
6-6. Note that the function from the previous exercise and the functions from §6.2.2/113 and
§6.2.3/115 do the same task. Merge these three analysis functions into a single function.
6-7. The portion of the grading analysis program from §6.2.1/110 that read and classified student records depending on whether they did (or did not) do all the homework is similar to the problem we solved in extract_fails. Write a function to handle this subproblem.
6-8. Write a single function that can be used to classify students based on criteria of your choice. Test this function by using it in place of the extract_fails program, and use it in the program to analyze student grades.
6-9. Use a library algorithm to concatenate all the elements of a vector<string>.