Tips and Tricks
Creating Custom View Adaptors
Single-Argument View Adaptors
One of the greatest merits of using the range-v3 library is that one can freely combine several view adaptors/generators via the |
-operator. Moreover, if there is a sequence of view adaptors that you are using regularly, you can readily create a custom view to save you typing. See for example the following snippet:
using namespace ranges::views;
auto filter_odd_numbers_and_square =
filter([](const auto& val) { return val % 2; }) |
transform([](const auto& val) { return val * val; });
const std::vector<int> vec{1, 2, 3, 4, 5};
auto rng = vec | filter_odd_numbers_and_square;
std::cout << rng << '\n'; // prints: [1, 9, 25]
In this example, we are combining the filter
and transform
view adaptors to a new custom adpator called filter_odd_numbers_and_square
. As the name suggests, this adaptor takes a range as an input, removes all values that are even, and then finally squares these values. You might be questioning the purpose of this admittedly quite constructed example, but I hope that you appreciate the ease with which we can combine multiple view adaptors.
Multi-Argument View Adaptors
In the previous section, we have created a custom view adaptor by simply combining two existing adaptors. In particular, note that the |
-operator works readily for the new view adaptor, i.e., vec | filter_odd_numbers_and_square
worked out of the box. Now, let's say we want to write a slightly more generic view adaptor. For instance, something along the lines of vec | add_number(5)
. Instead of always adding a specified number, we want to pass the value to be added to our custom view adaptor. How might we go about this?
auto add_number = [](auto&& rng, const auto& to_add) {
return rng | transform([&](const auto& v) { return v + to_add; });
};
const std::vector<int> vec{1, 2, 3, 4, 5};
auto rng = add_number(vec, 5);
std::cout << rng << '\n'; // prints: [6, 7, 8, 9, 10]
In the snippet above, we have wrapped the transform
view adaptor with a lambda function. Inside the lambda function we then pipe the range rng
to a view adaptor that adds the value of to_add
to all elements of the original range. Note the extensive use of the auto
keyword. That way, the lambda function works with any kind of range as an input. For example. we could easily input a std::set<double>
and add 5.2
to each element.
However, while the construction of the lambda does not pose much overhead, we have to use the new view adaptor as add_number(vec, 5)
. The |
-operator is not automatically created for us, i.e., the following does not compile vec | add_number(5)
. This is because the add_number
is not a view adaptor, but a lambda function; and lambda functions take care of a lot of code generation under the hood for us, they (unfortunately) do not implement the |
-operator for us. Luckily, the range-v3 library provides a handy utility function for us, i.e., make_pipeable
.
auto add_number_pipeable = [&](const auto& to_add) {
return ranges::make_pipeable([&](auto&& rng) {
return add_number(std::forward<decltype(rng)>(rng), to_add);
});
};
// pipe-operator can now be used with the new wrapper function.
auto rng2 = vec | add_number_pipeable(5);
std::cout << rng2 << '\n'; // prints: [6, 7, 8, 9, 10]
We could now leave this example as is, but I see the following problems:
- The
add_number
function does not resemble a real-world use case. While it was good for understanding the basics, I think we can find a more appropriate example that we actually might find useful. - As lambda functions cannot be overloaded, we had to choose a different name for
add_number_pipeable
. While this name was descriptive for our small example, I don't want to add the_pipeable
suffix to all my custom view adaptors that implement the|
-operator. Moreover, maybe not everyone prefers the|
-operator, and I don't want to have different names for functions that do exactly the same but use a slightly different syntax.
Here is an example that fixes both of these issues:
namespace rv = ranges::views;
// Adds an element to the beginning of a range.
template <typename Rng, typename T>
auto prepend(Rng&& rng, T t) {
return rv::concat(rv::single(std::move(t)), std::forward<decltype(rng)>(rng));
}
// Makes the `prepend` view adaptor pipeable.
template <typename T>
auto prepend(T t) {
return ranges::make_pipeable([t_ = std::move(t)](auto&& rng) {
return prepend(std::forward<decltype(rng)>(rng), std::move(t_));
});
}
int main() {
const std::vector<int> vec{1, 2, 3, 4, 5};
auto rng = vec | prepend(0); // or: prepend(vec, 0)
std::cout << rng << '\n'; // prints: [0, 1, 2, 3, 4, 5]
}
In the snippet above, we define a custom view adaptor that prepends any element to an already existing range - this is quite handy. Particularly, this circumvents the issue that the concat
view adaptor is not pipeable. As the concat
adaptor takes a variable number of arguments, it would not make sense to have a |
-operator. But if we only want to prepend a single element, it very much does make sense.
View Adaptors from Scratch
If neither of the two options above suit your needs, there is a third way to implement your custom view. That is, writing the view adaptor from scratch. While there are still some utilities in the range-v3 library that aid you along the process, this example is currently out of scope for this document. Maybe I will add such an example in the future. If you are still curious, I would recommend the following two sources:
interleave
view adaptor: In Eric Niebler's example on writing a CLI calender with range-v3, he implements theinterleave
view adaptor: https://github.com/ericniebler/range-v3/blob/master/example/calendar.cpp#L192. However, this view adaptor did not make it (yet?) to the official list of view adaptors, as he himself mentioned on Stackoverflow: https://stackoverflow.com/a/54025830
Debugging
[!NOTE] Debugging view Would be great to have a view that explains what is going on and prints some output information.
auto
Keyword is Your Friend
The concatenation of multiple view adaptors is encoded via the type.
Hence, it is almost impossible to use an explicit type for a range.
Use auto
liberally - for declaring variables and as return type.
Printing std::vector
to a stream
If the contained type can be printed to a stream, e.g., most primitive types, ranges can easily printed via std::cout << rng
.
To achieve the same for a std::vector
, just wrap the vector in the ranges::views::all
view adaptor: std::cout << ranges::views::all(v)
.
When to use actions
vs views
As mentioned above, actions
act eagerly on the underlying container while views
are evaluated lazily. So, when would we want to use either one? I used to prefer views
and almost rarely used actions
. However, the following pattern occurred regularly:
const auto tmp = computeSomething();
const auto vec = tmp | views::take(5) | ranges::to_vector;
Note that I needed to create the temporary variable rng
. This is because computeSomething()
is an r-value and thus cannot be piped into a view range adaptor. However, with actions
this is not a problem:
const auto vec = computeSomething() | actions::take(5);
Moreover, the following fails to compile:
auto tmp = computeSomething();
const auto vec = tmp | actions::take(5);
This is because tmp
would need to be copied, which can be inefficient. Hence, one can only pipe r-values into actions
, so that the following code works:
auto tmp = computeSomething();
const auto vec = std::move(tmp) | actions::take(5);
Note that std::move
does nothing if the passed variable is declared const
.
Converting to Various Containers
std::vector
:rng | ranges::to<std::vector<double>>
orrng | ranges::to_vector
for shortstd::set
:rng = vec | ranges::views::unique;