2

The C++20 standard says in [range.adaptors.general] that range adaptors

evaluate lazily as the resulting view is iterated.

On the other hand, there is a remark in [range.filter.view] for filter_view's begin function mentioning caching the result. To what degree are the adaptors lazy then?

When executing following code:

#include <iostream>
#include <ranges>

void print(std::ranges::range auto&& r)
{
    for (const auto& item : r)
    {
        std::cout << item << ", ";
    }
    std::cout << " <end of range>\n";
}

int main()
{
    using namespace std::ranges;

    bool filter = false;
    
    auto v = iota_view{4, 10} | views::filter([&filter](auto item){return filter;});

    // multipass guarantee
    static_assert(std::ranges::forward_range<decltype(v)>);

    filter = true;
    print(v);

    filter = false;
    print(v);

    filter = true;
    print(v);
}

Is there a guarantee that adaptor will respect the value of filter variable? If not, what kind of behavior am I invoking and where is it stated?

neonxc
  • 802
  • 9
  • 21

3 Answers3

7

Remember that, in the C++ iterator model, positioning and access are two distinct operations. However, a filtering iterator is one whose position is based on accessing the range it is filtering. That is the nature of the iterator.

To find the beginning of a filtered range requires finding the first position in the underlying range that matches the filter condition. Just as finding the next element in a filtered range requires iterating until you get to another iterator that matches the filter condition.

So getting the beginning iterator for a filtered range requires accessing at least one element of that range. A filtering iterator is as lazy as it is possible to make it while still doing its job.

However, your specific code exhibits UB, because your predicate is not a regular_invocable. The standard explicitly requires:

The invoke function call expression shall be equality-preserving ([concepts.equality])

Where that means:

An expression is equality-preserving if, given equal inputs, the expression results in equal outputs. The inputs to an expression are the set of the expression's operands. The output of an expression is the expression's result and all operands modified by the expression.

You violated that requirement by changing the behavior of the predicate.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
4

Is there a guarantee that adaptor will respect the value of filter variable?

No.

If not, what kind of behavior am I invoking and where is it stated?

This is IFNDR by [res.on.requirements]/3:

If the semantic requirements of a declaration's constraints ([structure.requirements]) are not modeled at the point of use, the program is ill-formed, no diagnostic required.

In particular, filter_view requires the predicate evaluation to be equality-preserving (via indirect_­unary_­predicate which requires predicate which requires regular_invocable). Something whose result can spontaneously change doe not meet this requirement - see [concepts.equality]/3.

T.C.
  • 133,968
  • 17
  • 288
  • 421
-2

Demo is the best: https://godbolt.org/z/WxrsfTrve

int squreIt(int x)
{
    std::cout << "squreIt(" << x << ")\n";
    return x * x;
}

int main()
{
    std::array a{4, 3, 2, 1, 0, 5, 6};

    for (auto x : a | std::views::transform(squreIt) | std::views::drop(3)) {
        std::cout << "Result = " << x << '\n';
    }
    std::cout << "---------\n";
    for (auto x : a | std::views::drop(3) | std::views::transform(squreIt)) {
        std::cout << "Result = " << x << '\n';
    }
    return 0;
}

Note squreIt is called only for items which are required for printing no matter of order of views. This demonstrates laziness.

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • 2
    I'm sorry to disagree. Demoing might and might not reflect the standard guarantees, especially in case of new features. – neonxc Mar 23 '21 at 17:33