Home > OS >  Can ranges::equal be used as a predicate for a pair of transform_view ranges?
Can ranges::equal be used as a predicate for a pair of transform_view ranges?

Time:04-19

Example Program and Compiler Errors

The following code produces two compiler errors (MSVC 2022 compiling with /std:c latest because <ranges> isn't yet enabled for /std:c 20 at time of writing this question):

error C2672: 'operator __surrogate_func': no matching overloaded function found
error C7602: 'std::ranges::_Equal_fn::operator ()': the associated constraints are not satisfied
#include <iostream>
#include <ranges>
#include <algorithm>
#include <string>
#include <cctype>

int main() {
    using std::views::transform;
    std::vector<std::string> foo{ "THe", "CaT", "SaT", "oN", "THe", "MaT" };
    std::vector<std::string> bar{ "The", "Cat", "Sat", "On", "The", "Mat" };

    bool are_equal = std::ranges::equal(
        foo | transform(transform(std::tolower)),
        bar | transform(transform(std::tolower)),
        std::ranges::equal
    );

    std::cout 
        << "vectors 'foo' and 'bar' are equal? - "
        << std::boolalpha << are_equal << std::endl;
}

Analysis

My guess regarding the first error __surrogate_func appears to be attempting to use operator== between the two transform_view::iterators (As far as I can tell, the iterator defines operator==).

The second error appears to relate to a constraint for std::ranges::equal::operator().

As far as I can tell, a constraint does not seem to be satisfied by transform_view, or perhaps more specifically, transform_view::iterator.

Looking at the source for ..\include\algorithm, ranges::equal is an instance of class _Equal_fn, with _Equal_fn::operator() having constraint requires indirectly_comparable<iterator_t<_Rng1>, iterator_t<_Rng2>.

I'm struggling to find anything on cppreference/ranges/transform_view to suggest that the transform_view::iterator is required to conform to this constraint, so I assume it's not supposed to.

However the implementation _Equal_fn clearly behaves as-expected when bypassed using a lambda without any constraints to wrap ranges::equal. This alternative implementation compiles in MSVC and produces the expected result:

    auto lambda_equal = [](const auto& lhs, const auto& rhs) {
        return std::ranges::equal(lhs, rhs);
    };

    // Now it compiles (MSVC  )
    bool are_equal = std::ranges::equal(
        foo | transform(transform(std::tolower)),
        bar | transform(transform(std::tolower)),
        lambda_equal
    );

(Which could be made a little more generic using a python-esque decorator pattern to make it generic enough for any other binary functionoid):

constexpr auto binary_invocable = [](const auto& fn) {
    return [&fn](const auto& lhs, const auto& rhs) {
        return fn(lhs, rhs);
    };
};
    // Now it compiles (MSVC  )
    bool are_equal = std::ranges::equal(
        foo | transform(transform(std::tolower)),
        bar | transform(transform(std::tolower)),
        binary_invocable(std::ranges::equal)
    );

Question:

On the basis that the problem appears to be with the constraint and not a lack of an iterator::operator== (), is there any way, using standard library functions/functors to either wrap range::equal or to wrap transform_view, as a means to satisfying the indirectly_comparable<iterator_t<Rng1>, iterator_t<Rng1> constraint?

CodePudding user response:

Can ranges::equal be used as a predicate for a pair of transform_view ranges?

No, it cannot. This is because ranges::equal is colloquially referred to as a niebloid (cppref, blog).

In the standard, they are defined in [algorithms.requirements]/2:

The entities defined in the std​::​ranges namespace in this Clause are not found by argument-dependent name lookup ([basic.lookup.argdep]). When found by unqualified ([basic.lookup.unqual]) name lookup for the postfix-expression in a function call ([expr.call]), they inhibit argument-dependent name lookup.

That's it. Namely, ranges::equal is still presented as a function template - just as this special kind of function template that inhibits, and is not found by, argument-dependent lookup.

The only way to implement that in C today is as a function object. But they're not specified as function objects, so you cannot rely on function-object-like properties. Like copyability. libstdc simply implements them as empty function objects, but MSVC deliberately chooses to not provide any functionality not specified in the standard. Hence in MSVC's implementation, ranges::equal is not copyable -- because it is not specified to be an object, it's specified as a function template, and you cannot use function templates that way.

CodePudding user response:

The issue can be reduced to

#include <algorithm>

static_assert(
  std::copy_constructible<decltype(std::ranges::equal)>);

Since ranges::equal requires Pred must satisfy indirectly_comparable<I1, I2, Pred, Proj1, Proj2>, which indirectly requires Pred must satisfy copy_constructible, so the constraints are not satisfied under MSVC. The standard doesn't require niebloid to be copyable, so both gcc and MSVC are correct.

The reason why MSVC's function objects are not copyable is that they all inherit _Not_quite_object, which is implemented as

class _Not_quite_object {
  _Not_quite_object(const _Not_quite_object&) = delete;
  _Not_quite_object& operator=(const _Not_quite_object&) = delete;
};

As stated in its comments

Some overload sets in the library have the property that their constituent function templates are not visible to argument-dependent name lookup (ADL) and that they inhibit ADL when found via unqualified name lookup.
This property allows these overload sets to be implemented as function objects. We derive such function objects from this type to remove some typical object-ish behaviors which helps users avoid depending on their non-specified object-ness.

So you cannot pass ranges::equal as a function object, because the standard doesn't require it to be.

  • Related