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?
1 Answer 1
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 using
s 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.
-
\$\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\$Sourav Kannantha B– Sourav Kannantha B2023年02月17日 17:53:50 +00:00Commented 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\$Sourav Kannantha B– Sourav Kannantha B2023年02月17日 17:55:18 +00:00Commented 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 usedstd::equal
instead ofstd::ranges::equal
just to keep them symmetrical. \$\endgroup\$Sourav Kannantha B– Sourav Kannantha B2023年02月17日 17:58:08 +00:00Commented 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 causestd::ranges::begin
to be invoked since it is a niebloid? \$\endgroup\$Sourav Kannantha B– Sourav Kannantha B2023年02月17日 18:02:12 +00:00Commented 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\$Toby Speight– Toby Speight2023年02月18日 09:50:39 +00:00Commented Feb 18, 2023 at 9:50
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\$*
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\$valarray
does, to allow maximum SIMD optimization. However, you could derive a subclass ofstd::span
orstd::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\$