4
\$\begingroup\$

If you want to lexicographically compare two vectors in C++, you can simply write vec1 <=> vec2. But instead, if you want to compare them reversed, it is not as simple as writing (vec1 | std::views::reverse) <=> (vec2 | std::views::reverse). So, I decided to write comparison operators for ranges in C++.

This is what I came up with:

inline constexpr auto operator==(const std::ranges::range auto& range1,
                                 const std::ranges::range auto& range2)
  -> decltype(begin(range1) == begin(range2)) {
    return std::equal(
      begin(range1), end(range1), begin(range2), end(range2));
}

inline constexpr auto operator<=>(const std::ranges::range auto& range1,
                                  const std::ranges::range auto& range2)
  -> decltype(begin(range1) <=> begin(range2)) {
    return std::lexicographical_compare_three_way(
      begin(range1), end(range1), begin(range2), end(range2));
}

Are the above functions correct?

These are currently generating other equality and comparison operators synthetically. But should I explicitly declare them as defaulted? If want to do so, how?

The unqualified names begin, end in above code are calling functions in std:: namespace or std::ranges:: namespace? Should I explicitly prefix them with std::ranges:: namespace.

Should I put these in their own namespace? If I do so, then I have to explicitly write using namespace rangecomp;, since these are not being detected by even ADL. So what to do?

\$\endgroup\$
5
  • 1
    \$\begingroup\$ This isn’t so much a review of your code, but: when I think of "comparing ranges," I’m almost always blending two different ranges based on a predicate. That’s also what those operators mean for GCC vector extensions. So this notation will be very ambiguous. \$\endgroup\$
    – Davislor
    Commented Feb 18, 2023 at 3:04
  • \$\begingroup\$ @Davislor You meant element-wise comparisons? Yeah, that's a good point. But STL containers already have lexicographical comparison operators. I'm just extending that to ranges. Nonetheless, it's an opinionated decision. \$\endgroup\$ Commented Feb 18, 2023 at 4:38
  • \$\begingroup\$ @Davislor Actually there is std::valarray which does element-wise comparisons. But I'm not sure I can distinguish a range originated from valarray and that originated from vector. If you have any ideas, please tell me. \$\endgroup\$ Commented Feb 18, 2023 at 4:41
  • 1
    \$\begingroup\$ I remember one keynote lecture where the speaker showed an overloaded * operator for vectors and asked, is this code intuitive and obvious? How many of you think it means dot product? How many of you think it’s element-wise multiplication? Whenever he asks, half the people think it’s one, half the other, but they all think it’s obvious. Same with + meaning element-wise addition or concatenation. \$\endgroup\$
    – Davislor
    Commented Feb 18, 2023 at 4:42
  • 1
    \$\begingroup\$ Ranges inherently can’t have the kind of memory-alignment requirements that valarray does, to allow maximum SIMD optimization. However, you could derive a subclass of std::span or std::ranges::subrange that implements element-wise operations, and document, these operators are element-wise. You would probably want to implement them using expression templates. \$\endgroup\$
    – Davislor
    Commented Feb 18, 2023 at 4:44

1 Answer 1

5
\$\begingroup\$

It does seem quite bold to provide these operators as such wide-ranging templates, which match all ranges. Not necessarily wrong, but it makes me slightly nervous of unintended side-effects!

I do think we should at least be constraining to accept only input ranges, as std::equal() and std::lexicographical_compare_three_way() require.

I don't think we need to be writing the return types, as plain auto is fine here - both std::equal() and std::lexicographical_compare_three_way() return the types we want. Not doing so avoids us needing to write unqualified begin and end - we can make the usings local to the functions, rather than polluting global namespace.

If we're providing ==, we should also provide !=. We can't use = default, because it's a template, and if we leave it undefined, it will be synthesised from <=> rather than from ==.


Modified code

#include <algorithm>
#include <ranges>

inline constexpr auto operator==(const std::ranges::input_range auto& range1,
                                 const std::ranges::input_range auto& range2)
{
    using std::ranges::begin;
    using std::ranges::end;

    return std::equal(begin(range1), end(range1),
                      begin(range2), end(range2));
}

inline constexpr auto operator!=(const std::ranges::input_range auto& range1,
                                 const std::ranges::input_range auto& range2)
{
    return !(range1 == range2);
}

inline constexpr auto operator<=>(const std::ranges::input_range auto& range1,
                                  const std::ranges::input_range auto& range2)
{
    using std::ranges::begin;
    using std::ranges::end;

    return std::lexicographical_compare_three_way(begin(range1), end(range1),
                                                  begin(range2), end(range2));
}
#include <array>
#include <vector>
int main()
{
    int const a[] = { 1, 2, 3 };
    auto const v = std::vector{ 1, 2, 3 };
    auto const vr = std::vector{ 3, 2, 1 };

    return false
        | ((a | std::views::all) <=> a) != std::strong_ordering::equivalent
        | (a | std::views::reverse) != ( v | std::views::reverse)
        | (v | std::views::reverse) != vr
        | !(a == v)
        | (a <=> v) != std::strong_ordering::equivalent
        | v < a
        | v > a
        ;
}

Answers to questions

The functions seem reasonable, though std::ranges::equal() might make a better choice for the first (there's no std::ranges::lexicographical_compare_three_way yet, due to the algorithm arriving at the same time as Ranges).

We can't default the other comparison operators because they are templates. We could write them all, but I think it's better to let the compiler default them.

Don't inhibit ADL by calling std::ranges::begin specifically - have using within the function as shown. (I might be wrong here, as niebloids have special ADL behaviour).

Yes, declare a namespace, but don't using namespace unless the comparisons are the only things in that namespace. Client code should just alias the specific names needed, and in the smallest reasonable scope (e.g. using my_comparators::operator<=>;). Enclosing in a namespace allows applications greater control over which parts of their code will match these very broad templates.

The functions should be detected by ADL - unless the view types you are using are not in that namespace (e.g. the standard view adapters in std::ranges::views). If you can create a minimal example of ADL failing for non-niebloid types in the same namespace, that would make a good question for Stack Overflow.

\$\endgroup\$
9
  • \$\begingroup\$ I provided these operators as for all ranges since almost all (except span and mdspan) strictly ordered containers in the STL already provide these operators. So I thought any ranges on those can also benefit from these operators. \$\endgroup\$ Commented Feb 17, 2023 at 17:53
  • \$\begingroup\$ If we are providing ==, then compiler can generate != automatically, similar to other comparison operators. Why you think we need to specify it explicitly? \$\endgroup\$ Commented Feb 17, 2023 at 17:55
  • 1
    \$\begingroup\$ It's sad that they didn't add std::ranges::lexicographical_compare_three_way even in C++23. As you might have guessed, I used std::equal instead of std::ranges::equal just to keep them symmetrical. \$\endgroup\$ Commented Feb 17, 2023 at 17:58
  • \$\begingroup\$ "Don't inhibit ADL by calling std::ranges::begin specifically - have using within the function as shown." I didn't get this. If I'm correct, when I write just begin(range1), wouldn't this cause std::ranges::begin to be invoked since it is a niebloid? \$\endgroup\$ Commented Feb 17, 2023 at 18:02
  • 2
    \$\begingroup\$ Compilers can generate != from ==, but that's not automatic. Without that definition, != will be synthesized from <=> (and we can't just write = default, as that's not valid for template functions). Actually, it's probably sufficient just to provide <=> and let everything else be synthesized from it. \$\endgroup\$ Commented Feb 18, 2023 at 9:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.