ranges::equal 可以用作一对 transform_view 范围的谓词吗?

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

示例程序和编译器错误

以下代码产生两个编译器错误(MSVC++ 2022 使用 /std:c++latest 编译,因为在撰写此问题时 <ranges> 尚未启用 /std:c++20):

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;
}

分析

我对第一个错误 __surrogate_func 的猜测似乎是试图在两个 transform_view::iterator 之间使用 operator== (As far as I can tell, the iterator defines operator==).

第二个错误似乎与 std::ranges::equal::operator() 的约束有关。

据我所知,transform_view 似乎不满足约束,或者更具体地说,transform_view::iterator

查看 ..\include\algorithm 的源代码,ranges::equalclass _Equal_fn 的一个实例,_Equal_fn::operator() 具有约束 requires indirectly_comparable<iterator_t<_Rng1>, iterator_t<_Rng2>.

我正在努力在 cppreference/ranges/transform_view 上找到任何建议 transform_view::iterator 必须符合此约束的内容,因此我认为它不应该如此。

然而,当使用 lambda 绕过而没有任何约束来包装 ranges::equal 时,实现 _Equal_fn 的行为显然符合预期。此替代实现在 MSVC++ 中编译并产生预期结果:

    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
    );

(可以使用 python 风格的装饰器模式使其变得更通用一些,以使其对任何其他二进制 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)
    );

问题:

根据问题似乎与约束有关而不是缺少 iterator::operator== (),有没有办法使用标准库 functions/functors 来包装 range::equal或者包装 transform_view,作为满足 indirectly_comparable<iterator_t<Rng1>, iterator_t<Rng1> 约束的一种方式?

问题可以reduced to

#include <algorithm>

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

因为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 是可复制的,所以gcc和MSVC都是正确的

MSVC的函数对象之所以不可复制,是因为它们都继承了_Not_quite_object,实现为

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

如其 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.

因此您不能将 ranges::equal 作为函数对象传递,因为标准不要求它是。

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

不,不能。这是因为 ranges::equal 通俗地称为 niebloid (cppref, blog)。

在标准中,它们定义在[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.

就是这样。也就是说,ranges::equal 仍然作为函数模板呈现 - 正如这种特殊类型的函数模板会抑制 argument-dependent 查找,并且不会被 argument-dependent 查找找到。

如今在 C++ 中实现它的唯一方法是作为函数对象。但它们未指定 作为函数对象,因此您不能依赖 function-object-like 属性。比如可复制性。 libstdc++ 简单地将它们实现为空函数对象,但 MSVC 有意选择不提供标准中未指定的任何功能。因此在 MSVC 的实现中,ranges::equal 是不可复制的——因为它没有被指定为一个对象,它被指定为一个函数模板,你不能那样使用函数模板。