I have a filter which should be conditionally applied to some range and the condition is known at execution time only.
I can do this easy, putting the condition in the filter, but this leads to high frequency checking and complication of the resulting code (the real code is not so simple as in the example).
Demo:
#include <iostream>
#include <ranges>
#include <vector>
int main()
{
std::vector<int> v = { 0, 1, 2, 3, 4, 5, 6 };
const bool strategy_check_for_zero = true;
{
// Works just fine, but I want to get rid off high frequent check in lambda
auto selected = v | std::views::filter([=](const auto& v) { return !strategy_check_for_zero || v != 0; });
std::ranges::copy(selected, std::ostream_iterator<int>{std::cout, ", "});
std::cout << '\n';
}
{
auto selected2 = std::ranges::subrange(v);
if (strategy_check_for_zero) {
// Impossible because "selected" has abother type
selected2 = v | std::views::filter([=](const auto& v) { return v != 0; });
}
std::ranges::copy(selected2, std::ostream_iterator<int>{std::cout, ", "});
std::cout << '\n';
}
{
// Impossible because v and filtered v have different types
auto selected3 = strategy_check_for_zero ?
v | std::views::filter([=](const auto& v) { return v != 0; }) :
v;
std::ranges::copy(selected3, std::ostream_iterator<int>{std::cout, ", "});
std::cout << '\n';
}
}
Of course, real data in vector is more complex, hard to copy and vector’s size and large.
The question is, can this be done somehow in the second (preferably) or at least third way in the example below without expensive and fragile approaches like std::ranges::to<std::vector>
and type erasure?
To put in a nutshell, if I have many conditions which alter the range to be processed by the followed complex algorithms, what is the recommended way to provide the filtered (which means not only std::views::filter
, but many other types of filtering which could make hard embedding flag checking, like std::views::drop
, etc.)) range to the next steps of the code?
I would apply all my conditions one by one, narrowing the set to be used because of complex nature of their interaction, so the approach I am looking for should allow a kind of updating the range. Is that possible?
Even harder; sometimes I want to drop some filter if it results in empty set, so I need to know the result of filter applying before making the decision I am ready to take it.
And, would the solution change (be easier) if the condition is known in compile time?
As you noted, a filtered view is different type than a non-filtered view (and the types are not convertible).
You can always define a function object that can perform arbitrary work in the constructor in order to hit the performance you are (presumably) trying to achieve.
class Filter {
public:
Filter() {
m_strategy_check_for_zero = true /* complex logic */;
}
bool operator()(const auto& elem) const { return !m_strategy_check_for_zero || elem != 0; }
private:
bool m_strategy_check_for_zero;
};
int main()
{
bool strategy_check_for_zero = false;
std::vector<int> v = { 0, 1, 2, 3, 4, 5, 6 };
auto selected = v | std::views::filter(Filter{});
std::ranges::copy(selected, std::ostream_iterator<int>{std::cout, ", "});
return 0;
}
For the more general problem of expressing two ranges with run-time selection, here is an implementation for a choice
view. It's been quite helpful in our project and works for most cases (no claim that it is ready for worldwide deployment).
#include <cassert>
#include <ranges>
#include <iterator>
#include <vector>
#include <algorithm>
#include <iostream>
////////////////////////////////////////////////////////////////////////////////
template<std::ranges::view V1, std::ranges::view V2>
class choice_view : public std::ranges::view_interface<choice_view<V1, V2>> {
using Iter1 = std::ranges::iterator_t<V1>;
using Iter2 = std::ranges::iterator_t<V2>;
using Sentinel1 = std::ranges::sentinel_t<V1>;
using Sentinel2 = std::ranges::sentinel_t<V2>;
public:
class Iterator {
static constexpr bool allForward = std::ranges::forward_range<V1> && std::ranges::forward_range<V2>;
static constexpr bool allBidirectional =
std::ranges::bidirectional_range<V1> && std::ranges::bidirectional_range<V2>;
static constexpr bool allRandomAccess =
std::ranges::random_access_range<V1> && std::ranges::random_access_range<V2>;
friend class choice_view::Sentinel;
public:
using value_type = std::common_type_t<std::ranges::range_value_t<V1>, std::ranges::range_value_t<V2>>;
using difference_type =
std::common_type_t<std::ranges::range_difference_t<V1>, std::ranges::range_difference_t<V2>>;
using reference_type =
std::common_reference_t<std::ranges::range_reference_t<V1>, std::ranges::range_reference_t<V2>>;
Iterator(){};
Iterator(Iter1 itr1, Iter2 itr2, const choice_view* parent)
: m_iter1{std::move(itr1)}
, m_iter2{std::move(itr2)}
, m_parent{parent}
, m_use_first{m_parent->m_use_first} {};
reference_type operator*() const { return m_use_first ? *m_iter1 : *m_iter2; }
bool operator==(const Iterator& other) const
requires(std::equality_comparable<Iter1> && std::equality_comparable<Iter2>)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 == other.m_iter1 : m_iter2 == other.m_iter2;
}
bool operator!=(const Iterator& other) const
requires(std::equality_comparable<Iter1> && std::equality_comparable<Iter2>)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 != other.m_iter1 : m_iter2 != other.m_iter2;
}
Iterator& operator++() {
if (m_use_first) {
++m_iter1;
} else {
++m_iter2;
}
return *this;
}
void operator++(int) { ++(*this); }
Iterator operator++(int)
requires(allForward)
{
Iterator tmp = *this;
++(*this);
return tmp;
}
Iterator& operator--()
requires(allBidirectional)
{
if (m_use_first) {
--m_iter1;
} else {
--m_iter2;
}
return *this;
}
Iterator operator--(int)
requires(allBidirectional)
{
Iterator tmp = *this;
--(*this);
return tmp;
}
// Note: All the operators must be defined separately, since it is not guaranteed
// that operator <=> is defined for Iterator. Concrete example: boost::container::static_vector.
bool operator<(const Iterator& other) const
requires(allRandomAccess)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 < other.m_iter1 : m_iter2 < other.m_iter2;
}
bool operator<=(const Iterator& other) const
requires(allRandomAccess)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 <= other.m_iter1 : m_iter2 <= other.m_iter2;
}
bool operator>(const Iterator& other) const
requires(allRandomAccess)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 > other.m_iter1 : m_iter2 > other.m_iter2;
}
bool operator>=(const Iterator& other) const
requires(allRandomAccess)
{
assert(m_parent == other.m_parent);
return m_use_first ? m_iter1 >= other.m_iter1 : m_iter2 >= other.m_iter2;
}
reference_type operator[](difference_type x) const
requires(allRandomAccess)
{
return m_use_first ? reference_type(m_iter1[std::iter_difference_t<Iter1>(x)])
: reference_type(m_iter2[std::iter_difference_t<Iter2>(x)]);
}
Iterator& operator+=(difference_type x)
requires(allRandomAccess)
{
if (m_use_first) {
m_iter1 += std::iter_difference_t<Iter1>(x);
} else {
m_iter2 += std::iter_difference_t<Iter2>(x);
}
return *this;
}
Iterator& operator-=(difference_type x)
requires(allRandomAccess)
{
if (m_use_first) {
m_iter1 -= std::iter_difference_t<Iter1>(x);
} else {
m_iter2 -= std::iter_difference_t<Iter2>(x);
}
return *this;
}
difference_type operator-(const Iterator& other) const
requires(std::sized_sentinel_for<Iter1, Iter1> && std::sized_sentinel_for<Iter2, Iter2>)
{
assert(m_parent == other.m_parent);
return m_use_first ? difference_type(m_iter1 - other.m_iter1) : difference_type(m_iter2 - other.m_iter2);
}
friend Iterator operator+(const Iterator& i, difference_type x)
requires(allRandomAccess)
{
auto r = i;
r += x;
return r;
}
friend Iterator operator+(difference_type x, const Iterator& i)
requires(allRandomAccess)
{
auto r = i;
r += x;
return r;
}
friend Iterator operator-(const Iterator& i, difference_type x)
requires(allRandomAccess)
{
auto r = i;
r -= x;
return r;
}
friend Iterator operator-(difference_type x, const Iterator& i)
requires(allRandomAccess)
{
auto r = i;
r -= x;
return r;
}
private:
Iter1 m_iter1;
Iter2 m_iter2;
const choice_view* m_parent;
bool m_use_first;
};
class Sentinel {
public:
Sentinel() = default;
Sentinel(Sentinel1 sentinel1, Sentinel2 sentinel2, const choice_view* parent)
: m_sentinel1{std::move(sentinel1)}
, m_sentinel2{std::move(sentinel2)}
, m_parent{parent}
, m_use_first{m_parent->m_use_first} {};
bool operator==(const Iterator& itr) const {
assert(itr.m_parent == m_parent);
return m_use_first ? itr.m_iter1 == m_sentinel1 : itr.m_iter2 == m_sentinel2;
}
private:
Sentinel1 m_sentinel1;
Sentinel2 m_sentinel2;
const choice_view* m_parent;
bool m_use_first;
};
static_assert(std::sentinel_for<Sentinel, Iterator>);
choice_view() = default;
choice_view(V1 view1, V2 view2, bool useFirst)
: m_view1{std::move(view1)}, m_view2{std::move(view2)}, m_use_first{useFirst} {}
auto begin() { return Iterator{m_view1.begin(), m_view2.begin(), this}; }
auto end() {
if constexpr (std::ranges::common_range<V1> && std::ranges::common_range<V2>) {
return Iterator{m_view1.end(), m_view2.end(), this};
} else {
return Sentinel{m_view1.end(), m_view2.end(), this};
}
}
auto size() const
requires(std::ranges::sized_range<const V1> && std::ranges::sized_range<const V2>)
{
using CT = std::common_type_t<decltype(std::ranges::size(m_view1)), decltype(std::ranges::size(m_view2))>;
return m_use_first ? CT(std::ranges::size(m_view1)) : CT(std::ranges::size(m_view2));
}
private:
V1 m_view1;
V2 m_view2;
bool m_use_first;
};
// Choice view that can be used to dynamically select between two compatible views based on a run-time value.
inline constexpr auto choice =
[]<std::ranges::viewable_range R1, std::ranges::viewable_range R2>(bool useFirst, R1&& rng1, R2&& rng2) {
return choice_view(std::views::all(std::forward<R1>(rng1)), std::views::all(std::forward<R2>(rng2)), useFirst);
};
////////////////////////////////////////////////////////////////////////////////
int main()
{
bool strategy_check_for_zero = false;
std::vector<int> v{0,1,2,3};
auto selected = choice(strategy_check_for_zero,
v | std::views::filter([=](const auto& v) { return v != 0; }),
v);
std::ranges::copy(selected, std::ostream_iterator<int>{std::cout, ", "});
return 0;
}