Boost Range Highlights
Last week, I presented Boost Range for Humans: documentation for the Boost Range library with concrete code examples. This week we'll talk about some of the cool features in Boost Range.
irange
boost::irange()
is the C++ equivalent to Python's
range()
function. It returns a range object containing an
arithmetic series of numbers:
boost::irange(4, 10) -> {4, 5, 6, 7, 8, 9}
boost::irange(4, 10, 2) -> {4, 6, 8}
Together with indexed()
(see below), it serves as a range-based
alternative to the classic C for
loop.
combine
boost::combine()
takes two or more input ranges and creates a
zipped range – a range of tuples where each tuple contains corresponding
elements from each input range.
The input ranges must have equal size.
std::string str = "abcde";
std::vector<int> vec = {1, 2, 3, 4, 5};
for (const auto & zipped : boost::combine(str, vec)) {
char c; int i;
boost::tie(c, i) = zipped;
// Iterates over the pairs ('a', 1), ('b', 2), ...
}
Algorithms
Most if not all algorithms from the C++ standard library that apply to
containers (via begin/end iterator pairs) have been wrapped in Boost Range.
Examples include copy()
, remove()
, sort()
, count()
, find_if()
.
Adaptors
Adaptors are among the most interesting concepts Boost Range has to offer.
There are generally two ways to use adaptors, either via function syntax or via a pipe syntax. While the former is handy for simple cases, while the latter allows chaining data transformations into an easy-to-read pipeline.
bool is_even(int n) { return n % 2 == 0; }
const std::vector<int> vec = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Function-style call:
for (int i : boost::adaptors::filter(vec, is_even)) { ... }
// Pipe-style call:
for (int i : vec | boost::adaptors::filtered(is_even)) { ... }
To see the power of the latter syntax, consider a transformation pipeline:
int square(int n) { return n*n; }
std::map<int, int> input_map = { ... };
using boost::adaptors;
auto result = input_map | map_values
| filtered(is_even)
| transformed(square);
indexed
The boost::adaptors::indexed()
adapter warrants special mention: it is analogous to
Python's enumerate()
. Given a Range, it gives access to the elements as well
as their index within the range. Boost 1.56 or higher is required for this to
work properly.
accumulate
boost::accumulate()
by default sums all the items in an input
range, but other reduction functions can be supplied as well. Together with
range adapters, this makes map-reduce pipelines easy to write.
std::vector<int> vec = {1, 2, 3, 4, 5};
int product = boost::accumulate(vec, 1, std::multiplies<int>());
as_literal
boost::as_literal()
may be less a highlight and more of a crutch,
but it bears mentioning still. Boost Range functions accept a wide variety of
types, among them strings. C++ style strings (std::string
) always work as
expected, but with character arrays (char[]
), there is an ambiguity as to
whether the argument should be interpreted as array (including the terminal
'\0' character) or as string (excluding the terminator).
By default, Boost Range treats them as arrays, which they are, after all. In practice, this is often a pitfall for newcomers. If any string-related range operations don't work as expected, this is a common reason.
To force the library to treat character arrays as strings, they can be wrapped
in an as_literal()
call. Alternatively, the C strings can be cast to
std::string
as well.
Summary
There are several interesting aspects about Boost Range. It plays very well with C++11's range-based for loops and makes code operating on containers much easier to write and (most importantly) read. In addition, it makes it possible to lay out data processing pipelines a lot more clearly.
Container iteration and modification becomes as easy as it is in modern scripting languages, which is a huge, huge step for the C++ language.
Let's hope that C++17 brings similar capabilities in the standard library. Until then, Boost Range is the way to go, so check out the docs and try it yourself.
Comments