Search code examples
c++for-loopstd-rangesc++23

std::ranges::algorithms differ in semantics compared to range-based for() loop in regard to the usage of a particular view


I wanted to view a 3x3 grid column by column, so I figured I would use std::views::stride like so:

#include <array>
#include <iostream>
#include <ranges>

auto main() -> int {
    auto grid = std::array<std::array<int, 3>, 3>{{
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
    }};

    namespace vs = std::views;

    auto strideCount = grid.size();
    auto allElements = grid | vs::join;

    auto columns = vs::iota(0uz, strideCount)
                   | vs::transform([allElements, strideCount](auto n) {
                        return allElements
                                | vs::drop(n)
                                | vs::stride(strideCount);
                   });

    for (auto&& column : columns) {
        for (auto element : column) {
            std::cout << element << ' ';
        }
        std::cout << '\n';
    }
}

This works and prints:

1 4 7
2 5 8
3 6 9

That is, it prints every element column by column.

In my original example I wanted to test whether each column satisfies a given criteria. Instead of a range-based for () loop, I tried using std::ranges::all_of():

#include <algorithm>
#include <array>
#include <iostream>
#include <ranges>

auto main() -> int {
    auto grid = std::array<std::array<int, 3>, 3>{{
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
    }};

    namespace vs = std::views;

    auto strideCount = grid.size();
    auto allElements = grid | vs::join;

    auto columns = vs::iota(0uz, strideCount)
                   | vs::transform([allElements, strideCount](auto n) {
                        return allElements
                                | vs::drop(n)
                                | vs::stride(strideCount);
                   });

    std::ranges::all_of(columns, [](auto&& column) {
        for (auto element : column) {
            // logic that alters whether we return true or false
        }
        return true;
    });
}

The only difference is that instead of:

for (auto&& column : columns) {
    for (auto element : column) {
        std::cout << element << ' ';
    }
    std::cout << '\n';
}

I now have:

std::ranges::all_of(columns, [](auto&& column) {
    for (auto element : column) {
        // logic that alters whether we return true or false
    }
    return true;
});

With the appropriate #include <algorithm>.

However, this unfortunately does not work. It fails here (in the std::ranges::all_of() snippet):

for (auto element : column) {

With error claiming that:

error: passing 'const std::ranges::stride_view<std::ranges::drop_view<std::ranges::join_view<std::ranges::ref_view<std::array<std::array<int, 3>, 3> > > > >' as 'this' argument discards qualifiers [-fpermissive]

For both begin() and end().

However, if I instead of taking auto&& column take auto column in the lambda, the code compiles.

Why is it so? Why can I use forwarding reference for each column in the for () loop example and can't do that in the lambda argument in std::ranges::algorithm cases?

I am using gcc version 13.1.0 (MinGW-W64 x86_64-msvcrt-mcf-seh, built by Brecht Sanders) (output of g++ -v).


Solution

  • This is LWG 3996, for which P2997 specifically provides proposed wording.


    ranges::all_of requires the predicate Pred to satisfy indirect_unary_predicate<Pred, projected<I, Proj>>, which ultimately leads to

    invocable<Pred&, iter_common_reference_t<projected<I, Proj>>> 
    

    must be satisfied, where Proj is the projection function.

    Class template std::projected is a helper type used to construct a new iterator type whose reference type is the result of applying Proj (std::identity in most cases) to the iter_reference_t<I>, which facilitates the spelling of the requirements of the "rangified" algorithm ([projected]):

    namespace std {
      template<class I, class Proj>
      struct projected-impl {                               // exposition only
        struct type {                                       // exposition only
          using value_type = remove_cvref_t<indirect_result_t<Proj&, I>>;
          using difference_type = iter_difference_t<I>;     // present only if I
                                                            // models weakly_incrementable
          indirect_result_t<Proj&, I> operator*() const;    // not defined
        };
      };
    
      template<indirectly_readable I, indirectly_regular_unary_invocable<I> Proj>
        using projected = projected-impl<I, Proj>::type;
    }
    

    Let's go back to invocable<Pred&, iter_common_reference_t<projected<I, Proj>>>.

    In your example, the original iterator I's reference and value_type are both prvalue stride_view, so its common reference iter_common_reference_t<I> is also stride_view.

    However, projected<I, Proj>'s reference and value_type are stride_view&& and stride_view respectively, which makes the result of iter_common_reference_t<projected<I, Proj>> end up being calculated as const stride_view&. Since stride_view is not const-iterable, this results in a hard error during instantiation because we call const stride_view's begin inside an unconstrained lambda.

    This is a standard defect. It seems that we still need the proposed resolution of LWG 3859 to make projected<I, std::identity> just I, although it is marked as "Resolved by P2609R3", which is not the case in this case.

    The workaround is to use a dummy identity that returns prvalue so that the common reference of the projected iterator is still a prvalue (demo):

    std::ranges::all_of(columns, [](auto&& column) {
        for (auto element : column) {
            // logic that alters whether we return true or false
        }
        return true;
    }, [](auto r) { return r; });