© Ray Lischner 2020
R. LischnerExploring C++20https://doi.org/10.1007/978-1-4842-5961-0_10

10. Algorithms and Ranges

Ray Lischner1 
(1)
Ellicott City, MD, USA
 

The previous Exploration introduced vectors and ranges using std::ranges::sort to sort a vector of integers. This Exploration examines ranges in more depth and introduces more generic algorithms, which perform useful operations on ranges of objects.

Algorithms

The std::ranges::sort function is an example of a generic algorithm, so named because these functions implement common algorithms and operate generically. That is, they work for just about anything you can express as a sequential range of values. Most of the standard algorithms are declared in the <algorithm> header, although the <numeric> header contains a few that are numerically oriented.

The standard algorithms run the gamut of common programming activities: sorting, searching, copying, comparing, modifying, and more. Searches can be linear or binary. A number of functions, including std::ranges::sort, reorder elements within a sequence. No matter what they do, nearly all generic algorithms share some common features. (A few algorithms, such as std::max, std::min, and std::minmax, operate on values instead of ranges.) Ranges come in different flavors, depending on the type of the iterator and the nature of the ranged data.

A vector is an example of a sized range, that is, a range with a size that the C++ library can determine in constant time. Suppose a program defines a range of lines of text read from a file; the number of lines cannot be known beforehand, so such a range could not be a sized range.

The flavor of range also depends on the iterator type. C++ has six different kinds of iterators, but you can broadly group them into two categories: read and write.

A read iterator refers to a position in a sequence of values that enables reading from the sequence. Most algorithms require a read iterator with a corresponding sentinel in order to obtain the input data. Some algorithms are read-only and others can modify the iterated values.

Most algorithms also require a write iterator, more commonly known as an output iterator. Most algorithms use only the single output iterator instead of an output range. This is because the size of the output range is not necessarily known until the algorithm has run its course over its input.

If the input range is sized, an algorithm could use that information to set the size of an output range, but not all output ranges are sized. For example, writing the values of a vector to an output stream has a sized input but not a sized output. In order to keep algorithms generic, they rarely require a sized range as input and rarely accept a range as output.

Because a typical algorithm does not and cannot check for overflow of the output iterator, you must ensure the output sequence has sufficient room to accommodate everything the algorithm will write.

For example, the std::ranges::copy algorithm copies values from an input range to an output iterator. The function takes two arguments: an input range and output iterator. You must ensure the output has enough capacity. Call the resize member function to set the size of the output vector, as shown in Listing 10-1.
#include <cassert>
import <algorithm>;
import <vector>;
int main()
{
  std::vector<int> input{ 10, 20, 30 };
  std::vector<int> output{};
  output.resize(input.size());
  std::ranges::copy(input, output.begin());
  // Now output has a complete copy of input.
  assert(input == output);
}
Listing 10-1.

Demonstrating the std::ranges::copy Function

The assert function is a quick way to verify that what you think is true actually is true. You assert a logical statement, and if you are wrong, the program terminates with a message that identifies the assertion. The assert function is declared differently than the rest of the standard library, using #include <cassert> instead of import. The c means the C++ library inherits this header from the C standard library, and #include is how C imports declarations. Note that assert is one of the rare exceptions to the rule that standard library members begin with std::.

If the program is correct, it runs and exits normally. But if we make a mistake, the assertion triggers, and the programs fails with a message.

Test the program in Listing 10-1. Just to see what happens when an assertion fails, comment out the call to std::ranges::copy and run it again. Write down the message you get.
  • _____________________________________________________________

  • _____________________________________________________________

Also note the initialization of input. Listing 10-1 demonstrates another application of “universal initialization” (as introduced in Exploration 4). The comma-separated values inside the curly braces are used to initialize the elements of the vector.

Output Iterators

Being able to call resize() is fine if the output is a vector, but you can also use an output iterator to write values to a file or console. Take an output file such as std::cout and construct a std::ostream_iterator<int>{std::cout} object to turn it into an output iterator that prints int values. (Use import <iterator> to get declarations of the iterator-related declarations.) Even better, you can pass a string as a second argument, and the iterator writes that string after every value it writes. Copy Listing 9-1 and replace the output loop with a call to the copy() function that copies data to the standard output.
  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

Compare your program with Listing 10-2.
import <algorithm>;
import <iostream>;
import <iterator>;
import <vector>;
int main()
{
  std::vector<int> data;
  int element;
  while (std::cin >> element)
    data.emplace_back(element);
  std::ranges::sort(data);
  std::ranges::copy(data, std::ostream_iterator<int>{std::cout, " "});
}
Listing 10-2.

Demonstrating the std::ostream_iterator Class

Just as you can use the ostream_iterator to write a range to the standard output, you can also use the standard library to read values from the standard input directly into a range. What do you think the class is called?
  • _____________________________________________________________

Good guess, but remember that the input is a range and the output is just an iterator. Wouldn’t it be nice if the name of the class that treats the input as a range were std::input_range? But the name is actually std::ranges::istream_view. A view is a kind of range that is easy to copy or assign. By naming this type a view, it tells you that you can assign an istream_view variable without incurring a runtime penalty.

The job is now to use the std::ranges::copy() function to copy a range of int values from std::cin to the data vector. But here we run into a problem of setting the size of data to match the number of input values. The emplace_back() function extends the size of a vector to accommodate the new value, so how do we arrange to call emplace_back() for every element that is read from the istream_view?

The answer is a special kind of output iterator called std::back_inserter. Pass data as the argument to back_inserter, and every value written to the output iterator is added to the end of data. Now you have the pieces you need to rewrite Listing 10-2 so it does not contain any loops, but instead uses range function calls to do all of its work.
  • _______________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

  • _____________________________________________________________

Compare your program with mine in Listing 10-3.
 1 import <algorithm>;
 2 import <iostream>;
 3 import <iterator>;
 4 import <ranges>;
 5 import <vector>;
 6
 7 int main()
 8 {
 9   std::vector<int> data;
10   std::ranges::copy(std::ranges::istream_view<int>(std::cin),
11                     std::back_inserter(data));
12   std::ranges::sort(data);
13   std::ranges::copy(data, std::ostream_iterator<int>{std::cout, " "});
14 }
Listing 10-3.

Demonstrating the std::back_inserter Function

Ugly C++ truth: sometimes you need parentheses and sometimes you need curly braces and sometimes you need angle brackets and sometimes you need square brackets. How do you know when to use what? By memorizing the rules of the language and library. Okay, it’s not quite that bad. Square brackets are used for subscripts and angle brackets are used for types. But parentheses and curly braces can be downright confusing.

In Exploration 2, I enhorted you to use curly braces when initializing a variable. You do the same when creating an object, such as an iterator, on the fly. For example, you can create an ostream_iterator object and pass it to a function, such as copy. Because you are creating an object, you should use curly braces. But what about back_inserter? It is actually a function that takes its argument and uses it to create and return a back_insert_iterator object. By using the type (std::vector<int>) of its argument (data), back_inserter() can create the right kind of back_insert_iterator object. A result of this complication is that you need to remember what’s a function and what’s a type.

The standard library contains far too many functions to go into them here. Just for a taste of what is available, change the copy function on line 13 to unique_copy. How do you think this changes the program’s behavior?
  • _____________________________________________________________

  • _____________________________________________________________

Try it. If you don’t see any difference, try the following input:
10 42 3 1 42 5 3 10 3
Now you can see that unique_copy copies only one value when a number is repeated. So you should see the following output for the input earlier:
1
3
5
10
42
Let’s try one more function. Instead of sort(), call reverse(). Be sure to change unique_copy() back to copy() because unique_copy() works correctly only if the input is sorted. The name, reverse, tells you what to expect from the program. Try it to make sure you understand. What do you get for the same input as I?
3
10
3
5
42
1
3
42
10

In Exploration 9, I threw an ugly for loop at you just to scare you prior to introducing the ranged for loop. It’s time to start breaking down that ugly code and understand what the various pieces mean. The next Exploration takes a closer look at the handy increment (++) operator.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.199.130